Skip to content

程序启动逻辑

traefik/cmd/traefik/traefik:main 是整个程序的入口.

traefik 会从文件配置、命令行参数、环境变量读取相应的配置, 写到 TraefikCmdConfiguration 对象内

启动流程

  1. 实例化Command命令行解析对象, 绑定 TraefikCmdConfiguration 对象作为静态配置
  2. 实例化FileLoader, FlagLoader, EnvLoader解析文件、命令行参数、环境变量 填充静态配置
  3. 实例化healthcheck子命令
  4. 实例化cmdVersion子命令
  5. Execute 执行当前命令
    1. 启动 traefik 主命令
      1. 遍历 resource 对象, 执行对应的 load 方法, 初始化配置
        1. FileLoader
        2. FlagLoader
        3. EnvLoader
    2. healthcheck 子命令
    3. cmdVersion 子命令
    4. Resources Load
      1. File Load
      2. Flag Load
      3. Env Load
    5. cmd run

FileLoader

  1. 尝试从命令行参数解析配置文件

    1. 命令行参数解析语法按 --name value, --name=value 的格式解析 name:value 键值对
  2. loadConfigFiles 尝试寻找配置文件

    1. 遍历预设的文件路径, 查找文件路径
      1. 对路径名进行环境变量展开
      2. 若读取文件信息失败
        1. 文件不存在, 跳过
        2. 其他错误, 向上传递
      3. 读取文件信息成功后, 返回文件绝对路径
  3. 找到配置文件路径后, 读取文件并填充静态配置

FlagLoader

EnvLoader

Cmd run

  1. 通过 configureLogging 初始化日志对象
    1. 设置日志级别
    2. 设置日志格式
      1. 仅当静态配置中的Formatjson时, 使用jsonformatter, 否则使用text formatter
    3. 设置日志落盘
      1. 尝试创建目录, 若路径已存在文件, 则向上返回错误. 若路径已存在目录, 则直接结束创建
      2. 目录创建成功后, 打开文件, 并为logrus绑定Out对象
      3. 打开文件成功后, 为 logrus 绑定退出回调. 在退出回调中, 关闭对应文件.
  2. 设置 net/http内置库的 DefaultTransport 代理使用 ProxyFromEnvironment 从环境变量读取.
  3. 初始化 roundrobin 的权重
  4. SetEffectiveConfiguration 补全静态配置参数
    1. 添加默认 EntryPoint, 仅当配置文件未指定任何 EntryPoint 时, 会添加一个名为 http 监听 80 端口的默认 EntryPoint
    2. 检查配置参数, 当 API、Ping、Metrics 等内部服务有被使用的情况下, 添加一个名为traefik, 监听 8080 端口的 EntryPoint
  5. ValidateConfiguration 校验
    1. 启用了 ACMEResolverStorage 必须有值
    2. 所有 ACME ResolveracmeEmail 字段值必须相同
  6. 版本检查
    1. 首次启动延迟 10 分钟后检查 github 上的版本信息
    2. 第二次检查发生在程序启动后的 24 小时, 之后每次检查间隔 24 小时
    3. 版本检查检索到最新版本后, 只输出, 不做更新操作
    4. 版本检查仅在静态配置设置了 Global.CheckNewVersion 后启动
    5. 当程序版本属于Dev时, 执行版本检查的操作时直接跳过, 而不是请求 github
  7. 统计收集

    1. 仅当启用了 Global.SendAnonymousUsage 后, 才会开启统计收集
    2. 统计间隔同样按首次 10 分钟, 第二次 23h-10m, 第三次往后 24h.
  8. 服务器设置

    1. Add Provider Aggregator
      1. File、Docker、Rest、Http、Redis 等动态配置 Provider
    2. Add internal provider
    3. Add ACME Provider
      1. Make Tls Manager
      2. Add httpChallengeProvider
      3. Add tlsChallengeProvider
    4. Add Entrypoints
      1. Add Tcp Entry Points
      2. Add Udp Entry Points
    5. Add Pilot
    6. Create Plugin Builder
    7. Providers plugins
    8. Add Metrics
      1. Register Prometheus
      2. Register Datadog
      3. Register Statsd
      4. Register InfluxDB
      5. Register InfluxDB2
    9. Add Service Manager Factory
      1. Metrics Registry
      2. Round Tripper Manager
      3. Acme HTTP Handler
    10. Add Router Factory
      1. Setup Access Log
      2. Make Chain Builder
      3. Add Plugin Builder
      4. Add Tls Manager
    11. Add Listening
      1. Add Tls Listening
      2. Add Metrics Listening
      3. Add Server Transports
      4. Add Switch Router Listening
      5. Add Tls Challenge Listening
      6. Add Certificate Resolver Logs Listening
    12. 实例化 Server 对象

      1. Configure Signals syscall.SIGUSR1
    13. signal.NotifyContext初始化信号监听上下文

      1. 监听信号达到后触发退出
        1. 关闭 Ping 服务
        2. 关闭 traefik 服务
          1. tcpEntryPoints->Stop
          2. udpEntryPoints->Stop
          3. stopChan <- true
    14. traefik 服务启动
      1. tcpEntryPoints->Start
      2. udpEntryPoints->Start
      3. watcher->Start
      4. listenSignals 监听信号
        1. 监听 SIGUSR1 信号
          1. Closing and re-opening log files for rotation
    15. 守护进程设置
      1. SdNotify 初始化 Daemon
      2. SdWatchdogEnabled 启动 Watchdog
      3. healthcheck.Do
    16. <-stopChan 等待 traeifk 服务退出清理完成

注意事项

初始化配置规则

按照文件、命令行参数、环境变量的优先级规则读取配置, 且当读到配置并写入静态配置后, 就不会再读取下一个配置规则.

即三种配置规则, 最多只生效一种.

loadConfigFiles内 Finder 对配置文件查找路径优先级规则

根据 BasePathsExtensions , 按顺序进行查找.

目录优先级按照 /etc/traefik/traefik , $XDG_CONFIG_HOME/traefik , $HOME/.config/traefik , ./traefik 从高到低.

文件格式优先级按照 toml , yaml , yml 从高到低.

如果命令行参数存在 configPath , 则优先查找该文件.

finder := cli.Finder{
    BasePaths:  []string{"/etc/traefik/traefik", "$XDG_CONFIG_HOME/traefik", "$HOME/.config/traefik", "./traefik"},
    Extensions: []string{"toml", "yaml", "yml"},
}

func (f Finder) getPaths(configFile string) []string {
 var paths []string
 if strings.TrimSpace(configFile) != "" {
  paths = append(paths, configFile)
 }

 for _, basePath := range f.BasePaths {
  for _, ext := range f.Extensions {
   paths = append(paths, basePath+"."+ext)
  }
 }

 return paths
}

日志初始化

  1. 当初始化日志遇到错误时, 仅终止到当前, 已完成初始化的部分不会移除, 未完成初始化的部分直接忽略
  2. 当指定日志落盘时, 文本格式下, 彩色日志不起效.

默认配置参数补全

  1. 当存在 Docker Provider 时, 长轮询请求的间隔为 SwarmModeRefreshSeconds 默认为 15 秒.
  2. 所有超时相关的 Duration 为负数时, 设置为 0.
  3. 当存在 Rancher Provider时, 长轮询请求的间隔为 RefreshSeconds 默认为 15 秒.
  4. 仅当静态配置启用了 pilot 且配置了Token的情况下, SendAnonymousUsage 默认开启.
  5. 当静态配置未打开 Experimental 开关时, 关闭 KubernetesGatewayHTTP3 功能
  6. 当成功启用KubernetesGateway 功能后, 将静态配置绑定的EntryPoints复制到KubernetesGateway.EntryPoints
  7. ACME Provider分配合适的CAServer
    1. 未指定 CAServer 时, 默认值为https://acme-v02.api.letsencrypt.org/directory
    2. https://acme-v01.api.letsencrypt.org 替换成 https://acme-v02.api.letsencrypt.org
    3. https://acme-staging.api.letsencrypt.org 替换成 https://acme-staging-v02.api.letsencrypt.org

匿名统计

匿名统计除了包括版本信息等, 还包括了静态配置. 考虑到静态配置会比较敏感, 线上部署时尽量不要开启.

一些想法

Command. Configuration 对象的类型声明

type Command struct {
 Name           string
 Description    string
 Configuration  interface{}
 Resources      []ResourceLoader
 Run            func([]string) error
 CustomHelpFunc func(io.Writer, *Command) error
 Hidden         bool
 // AllowArg if not set, disallows any argument that is not a known command or a sub-command.
 AllowArg    bool
 subCommands []*Command
}

目前看下来, Command. Configuration 主要是存储静态配置.

在实例化的时候, 绑定的是 TraefikCmdConfiguration 对象.

在 loader_file 的相关函数内, 都已 interface{} 作为函数签名. 在实现上, 也更为复杂.

目前没想到这么声明的好处在哪里, 如果直接按 TraefikCmdConfiguration 声明, 实现会更容易, 可读性也会好上许多.

文件解析策略

// Decode decodes the given configuration file into the given element.
// The operation goes through three stages roughly summarized as:
// file contents -> tree of untyped nodes
// untyped nodes -> nodes augmented with metadata such as kind (inferred from element)
// "typed" nodes -> typed element.

在读取文件配置解析后写入静态配置时, 这个处理策略特别复杂.

比较困惑的一点还是为什么不声明明确的类型, 直接通过模型对象, 使用已有的解析工具来处理.

异常处理

// Go starts a recoverable goroutine.
func Go(goroutine func()) {
 GoWithRecover(goroutine, defaultRecoverGoroutine)
}

// GoWithRecover starts a recoverable goroutine using given customRecover() function.
func GoWithRecover(goroutine func(), customRecover func(err interface{})) {
 go func() {
  defer func() {
   if err := recover(); err != nil {
    customRecover(err)
   }
  }()
  goroutine()
 }()
}

func defaultRecoverGoroutine(err interface{}) {
 logger := log.WithoutContext()
 logger.Errorf("Error in Go routine: %s", err)
 logger.Errorf("Stack: %s", debug.Stack())
}

捕获并输出后台运行的 goroutine 错误信息.

基于信号的退出机制

 ctx, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)

if staticConfiguration.Ping != nil {
    staticConfiguration.Ping.WithContext(ctx)
}
func (h *Handler) WithContext(ctx context.Context) {
 go func() {
  <-ctx.Done()
  h.terminating = true
 }()

用 NotifyContext 中监听的信号来触发退出清理, 也是一种比较简明的方案.