traefik 源码学习(一)

1,338 阅读3分钟
原文链接: blog.logflows.top

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)
				}
				//...
			}
		}
	}
}

大致逻辑是:

  1. 读取容器信息
  2. 根据容器信息构造代理配置,configuration 变量其实是 frontend/backend/route 组成的配置信息
  3. 将新的配置发布出去
  4. 监听 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 这个库来实现的反向代理功能。