程序启动逻辑
traefik/cmd/traefik/traefik:main 是整个程序的入口.
traefik 会从文件配置、命令行参数、环境变量读取相应的配置, 写到 TraefikCmdConfiguration 对象内
启动流程
- 实例化
Command命令行解析对象, 绑定TraefikCmdConfiguration对象作为静态配置 - 实例化
FileLoader,FlagLoader,EnvLoader解析文件、命令行参数、环境变量 填充静态配置 - 实例化
healthcheck子命令 - 实例化
cmdVersion子命令 - Execute 执行当前命令
- 启动 traefik 主命令
- 遍历 resource 对象, 执行对应的 load 方法, 初始化配置
- FileLoader
- FlagLoader
- EnvLoader
- 遍历 resource 对象, 执行对应的 load 方法, 初始化配置
- healthcheck 子命令
- cmdVersion 子命令
- Resources Load
- File Load
- Flag Load
- Env Load
- cmd run
- 启动 traefik 主命令
FileLoader
-
尝试从命令行参数解析配置文件
- 命令行参数解析语法按
--name value,--name=value的格式解析name:value键值对
- 命令行参数解析语法按
-
loadConfigFiles尝试寻找配置文件- 遍历预设的文件路径, 查找文件路径
- 对路径名进行环境变量展开
- 若读取文件信息失败
- 文件不存在, 跳过
- 其他错误, 向上传递
- 读取文件信息成功后, 返回文件绝对路径
- 遍历预设的文件路径, 查找文件路径
-
找到配置文件路径后, 读取文件并填充静态配置
FlagLoader
EnvLoader
Cmd run
- 通过
configureLogging初始化日志对象- 设置日志级别
- 设置日志格式
- 仅当静态配置中的
Format为json时, 使用jsonformatter, 否则使用textformatter
- 仅当静态配置中的
- 设置日志落盘
- 尝试创建目录, 若路径已存在文件, 则向上返回错误. 若路径已存在目录, 则直接结束创建
- 目录创建成功后, 打开文件, 并为
logrus绑定Out对象 - 打开文件成功后, 为
logrus绑定退出回调. 在退出回调中, 关闭对应文件.
- 设置
net/http内置库的DefaultTransport代理使用ProxyFromEnvironment从环境变量读取. - 初始化
roundrobin的权重 SetEffectiveConfiguration补全静态配置参数- 添加默认
EntryPoint, 仅当配置文件未指定任何EntryPoint时, 会添加一个名为http监听80端口的默认EntryPoint - 检查配置参数, 当 API、Ping、Metrics 等内部服务有被使用的情况下, 添加一个名为
traefik, 监听8080端口的EntryPoint
- 添加默认
ValidateConfiguration校验- 启用了
ACME的Resolver的Storage必须有值 - 所有
ACME Resolver的acmeEmail字段值必须相同
- 启用了
- 版本检查
- 首次启动延迟 10 分钟后检查 github 上的版本信息
- 第二次检查发生在程序启动后的 24 小时, 之后每次检查间隔 24 小时
- 版本检查检索到最新版本后, 只输出, 不做更新操作
- 版本检查仅在静态配置设置了
Global.CheckNewVersion后启动 - 当程序版本属于
Dev时, 执行版本检查的操作时直接跳过, 而不是请求 github
-
统计收集
- 仅当启用了
Global.SendAnonymousUsage后, 才会开启统计收集 - 统计间隔同样按首次 10 分钟, 第二次 23h-10m, 第三次往后 24h.
- 仅当启用了
-
服务器设置
- Add Provider Aggregator
- File、Docker、Rest、Http、Redis 等动态配置 Provider
- Add internal provider
- Add ACME Provider
- Make Tls Manager
- Add httpChallengeProvider
- Add tlsChallengeProvider
- Add Entrypoints
- Add Tcp Entry Points
- Add Udp Entry Points
- Add Pilot
- Create Plugin Builder
- Providers plugins
- Add Metrics
- Register Prometheus
- Register Datadog
- Register Statsd
- Register InfluxDB
- Register InfluxDB2
- Add Service Manager Factory
- Metrics Registry
- Round Tripper Manager
- Acme HTTP Handler
- Add Router Factory
- Setup Access Log
- Make Chain Builder
- Add Plugin Builder
- Add Tls Manager
- Add Listening
- Add Tls Listening
- Add Metrics Listening
- Add Server Transports
- Add Switch Router Listening
- Add Tls Challenge Listening
- Add Certificate Resolver Logs Listening
-
实例化 Server 对象
- Configure Signals
syscall.SIGUSR1
- Configure Signals
-
signal.NotifyContext初始化信号监听上下文- 监听信号达到后触发退出
- 关闭
Ping服务 - 关闭 traefik 服务
tcpEntryPoints->StopudpEntryPoints->StopstopChan <- true
- 关闭
- 监听信号达到后触发退出
- traefik 服务启动
tcpEntryPoints->StartudpEntryPoints->Startwatcher->StartlistenSignals监听信号- 监听
SIGUSR1信号- Closing and re-opening log files for rotation
- 监听
- 守护进程设置
SdNotify初始化 DaemonSdWatchdogEnabled启动 Watchdoghealthcheck.Do
<-stopChan等待 traeifk 服务退出清理完成
- Add Provider Aggregator
注意事项
初始化配置规则
按照文件、命令行参数、环境变量的优先级规则读取配置, 且当读到配置并写入静态配置后, 就不会再读取下一个配置规则.
即三种配置规则, 最多只生效一种.
loadConfigFiles内 Finder 对配置文件查找路径优先级规则
根据 BasePaths 和 Extensions , 按顺序进行查找.
目录优先级按照 /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
}
日志初始化
- 当初始化日志遇到错误时, 仅终止到当前, 已完成初始化的部分不会移除, 未完成初始化的部分直接忽略
- 当指定日志落盘时, 文本格式下, 彩色日志不起效.
默认配置参数补全
- 当存在
Docker Provider时, 长轮询请求的间隔为SwarmModeRefreshSeconds默认为 15 秒. - 所有超时相关的
Duration为负数时, 设置为 0. - 当存在
Rancher Provider时, 长轮询请求的间隔为RefreshSeconds默认为 15 秒. - 仅当静态配置启用了
pilot且配置了Token的情况下,SendAnonymousUsage默认开启. - 当静态配置未打开
Experimental开关时, 关闭KubernetesGateway和HTTP3功能 - 当成功启用
KubernetesGateway功能后, 将静态配置绑定的EntryPoints复制到KubernetesGateway.EntryPoints - 为
ACME Provider分配合适的CAServer- 未指定 CAServer 时, 默认值为
https://acme-v02.api.letsencrypt.org/directory https://acme-v01.api.letsencrypt.org替换成https://acme-v02.api.letsencrypt.orghttps://acme-staging.api.letsencrypt.org替换成https://acme-staging-v02.api.letsencrypt.org
- 未指定 CAServer 时, 默认值为
匿名统计
匿名统计除了包括版本信息等, 还包括了静态配置. 考虑到静态配置会比较敏感, 线上部署时尽量不要开启.
一些想法
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 中监听的信号来触发退出清理, 也是一种比较简明的方案.