traefik 版本 v1.6.6
traefik 是一款反向代理服务,支持直接根据 docker、etcd 等服务动态生成反向代理和负载均衡配置。
核心概念
traefik 核心概念并不算多,主要是下面这些:
- Provider
- Frotend/Backend/Route
Provider 表示为 traefik 提供服务信息一方,比如前面文章当中提到的 Docker,traefik 可以读取到 Docker 的容器信息,并为容器分配外界可访问的域名。除了 Docker/k8s 这列容器管理服务以外,Docker 也可以从 etcd、consul 之类 key/value 存储服务当中读取配置,甚至也可以从 restful api 当中读取配置。
Frontend/Backend/Route 顾名思义,其实指的就是反向代理和负载均衡当中的【访问入口】【后端服务】【代理路由】的概念。以前面文章中提的 2048 为例,traefik 会为 2048 游戏容器提供生成以下配置:
{
"docker": {
"backends": {
"backend-2048": {
"servers": {
"server-2048-7f10269d49cc4c4dbc01411d034b3c6c": {
"url": "http://172.17.0.2:80",
"weight": 1
}
},
"loadBalancer": {
"method": "wrr"
}
}
},
"frontends": {
"frontend-Host-2048-jarvis-io-0": {
"entryPoints": [
"http"
],
"backend": "backend-2048",
"routes": {
"route-frontend-Host-2048-jarvis-io-0": {
"rule": "Host:2048.jarvis.io"
}
},
"passHostHeader": true,
"priority": 0,
"basicAuth": []
}
}
}
}
翻译成 nginx 的配置大概就是下面这样:
upstream backend {
server localhost:8080;
}
server {
server_name {domain};
listen 80;
location / {
proxy_pass http://backend;
}
}
Backend 类似 nginx 的 upstream,代表一组提供相同功能的服务。Frontend/Route 的组合和 server 配置块类似,表示一组反向代理的规则。
核心逻辑
启动流程分析
traefik 的入口在 cmd/traefik/traefik.go 的 main 行数当中,main 函数主要是配置了一些命令行参数,然后启动 Server ,真正的启动逻辑在 server/server.go#Start 方法当中:
func (s *Server) Start() {
s.startHTTPServers()
s.startLeadership()
s.routinesPool.Go(func(stop chan bool) {
s.listenProviders(stop)
})
s.routinesPool.Go(func(stop chan bool) {
s.listenConfigurations(stop)
})
s.startProvider()
go s.listenSignals()
}
从代码来看,Start 方法启动了多个协程:
- startHTTPServers
- startProvider
- listenProviders
- listenConfiguration
其中最关键的是 startProvider 和 listenConfiguration,它们分别负责:
- Provider 的监听和配置解析
- 反向代理、负载均衡动态更新模块
Provider 的监听和配置解析
上面 startProvider 方法启动了协程,读取 Provider 的配置信息,并开始监听 Provider。startProvider 方法会调用 server.provider 属性的 Provide 方法,server.provider 的类型是一个 interface:
type Provider interface {
// Provide allows the provider to provide configurations to traefik
// using the given configuration channel.
Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error
}
这也说明,想要支持 docker、etcd 等各种配置来源,只需要实现 Provider 接口即可。
看一下 Docker Provider 的实现,精简代码如下:
func (p *Provider) Provide(configurationChan chan<- types.ConfigMessage, pool *safe.Pool, constraints types.Constraints) error {
//...
dockerClient, err := p.createClient()
//...
dockerDataList, err = listContainers(ctx, dockerClient)
//...
configuration := p.buildConfiguration(dockerDataList)
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
if p.Watch {
//...
eventsc, errc := dockerClient.Events(ctx, options)
//...
startStopHandle := func(m eventtypes.Message) {
//...
containers, err := listContainers(ctx, dockerClient)
//...
configuration := p.buildConfiguration(containers)
if configuration != nil {
configurationChan <- types.ConfigMessage{
ProviderName: "docker",
Configuration: configuration,
}
}
}
for {
select {
case event := <-eventsc:
if event.Action == "start" ||
event.Action == "die" ||
strings.HasPrefix(event.Action, "health_status") {
startStopHandle(event)
}
//...
}
}
}
}
大致逻辑是:
- 读取容器信息
- 根据容器信息构造代理配置,configuration 变量其实是 frontend/backend/route 组成的配置信息
- 将新的配置发布出去
- 监听 docker 容器变化,当有容器启动或者停止的时候,重复步骤 1~3
关于 configuration 的生成细节,后续会整理出来。
反向代理、负载均衡动态更新
通过 listenConfigurations 方法可以找到配置更新的逻辑,核心逻辑在 server/server.go#loadConfig 方法当中,loadConfig 方法代码细节较多,但是大致能够理解,loadConfig 中重新配置了负载均衡和反向代理以及很多 middleware 的配置:
func (s *Server) loadConfig(configurations types.Configurations, globalConfiguration configuration.GlobalConfiguration) (map[string]*serverEntryPoint, error) {
serverEntryPoints := s.buildEntryPoints(globalConfiguration)
backends := map[string]http.Handler{}
for providerName, config := range configurations {
frontendNames := sortedFrontendNamesForConfig(config)
frontend:
for _, frontendName := range frontendNames {
frontend := config.Frontends[frontendName]
for _, entryPointName := range frontend.EntryPoints {
entryPoint := globalConfiguration.EntryPoints[entryPointName]
if backends[entryPointName+providerName+frontend.Backend] == nil {
n := negroni.New()
//...
var rr *roundrobin.RoundRobin
rr, _ = roundrobin.New(fwd)
s.configureLBServers(rr, config, frontend)
lbMethod, err := types.NewLoadBalancerMethod(config.Backends[frontend.Backend].LoadBalancer)
// ...
var lb http.Handler
switch lbMethod {
//...
case types.Wrr:
//...
log.Debugf("Creating load-balancer wrr")
lb = middlewares.NewEmptyBackendHandler(rr, lb)
}
//...
n.UseHandler(lb)
backends[entryPointName+providerName+frontend.Backend] = n
s.wireFrontendBackend(newServerRoute, backends[entryPointName+providerName+frontend.Backend])
}
}
}
}
//...
}
从上述代码当中可以看到,最后是通过 wireFrontendBackend 方法最终更新 http handler ,让配置生效。从代码当中也能看出,traefik 主要是使用 oxy 这个库来实现的反向代理功能。