众所周知,Traefik 是云原生生态中的一个爆款的反向代理和负载均衡器。我们无论如何定义、赞美它都不为过。毫无疑问,基于传统的反向代理组件而言,真正使 Traefik 与 Nginx,Haproxy 最为关键的不同之处在于其“开箱即用”的功能,即它的自适应和动态可配置性。不仅如此,相比较而言,Traefik 最为核心的部分可能是它做自动服务发现、灰度发布等能力。
在实际的业务场景规划中,如果我们将 Trafik 放在 Docker,Kubernetes,甚至是传统的 VM / 裸金属部署,并展示如何获取有关运行服务的信息,它将自动将它们暴露在集群外面。当然,如果我们有其他特殊的需求,那么可能需要遵循一些规范......
在 Traefik 2.x 发布的特性中我们了解到除了其固有的基础功能之外,其还支持一些其他的高级特性,例如,中间件,流量复制及金丝雀发布等等。本文就着重针对灰度发布此高级功能进行简要解析。
灰度发布,我们通常意义上将其也会称之为“金丝雀发布(Canary)”,其本质主要是让一部分测试的服务也参与到线上去,经过测试验证并观察其是否满足上线需求,从而达到与线上环境保持一致的部署策略模型。
基于我们的线上环境,若我们的部署规模相对较小,最多只有一位数的机器,并且由于种种原因,我们无法基于云平台享受无服务器容器技术带来的便利、高效,那么将 Docker 与 Traefik 结合可能将会是一个最为理想的选择。由于资源需求和编排器本身固有的复杂性,使用成熟的编排器(如 Kubernetes 或 Mesos )进行如此大规模的部署可能会有些过头。但是,我们将坚持过时的解决方案的事实并不意味着我们不想从现代发展的最佳实践中受益。
因此,为了简单起见,想象一下,假设有这样一种场景:当前我们只有一台机器。有一个 Docker 守护进程在其上运行,还有一个 Traefik 容器在主机的端口 80(或443,无论 80 或 443 皆可)上侦听。我们想在这台机器上部署我们的服务。然而,我们也希望通过应用金丝雀部署技术安全地发布新版本。
此时,我们可如下场景解析,比如,现在我们有两个为 v1.0.1 和 v1.0.2 两个不同版本的 X 微服务,我们希望通过 Traefik 来控制我们的流量转发:将 4⁄5 的流量路由到 v1.0.2,剩余 1/5 的流量路由到 v1.0.1 上面去,这个时候就可以利用 Traefik 2.0 中提供的带权重的轮询(WRR)来实现该功能。其简要示意图如下所示:
因此,我们需要让 Traefik 在相同服务的 Docker 容器之间进行加权负载平衡。如果我们能够在一台机器上解决负载平衡问题,我们只需将其扩展到集群的其他部分,如下参考示意图所示:
如果 Traefik 代理的每个容器服务实例都得到或多或少相同数量的请求,那么我们就可以在整个集群中实现所需的灰度请求的份额。
所有这些代理类型的软件在架构上看起来或多或少都是一样的。其基本的处理逻辑总是基于以下规则:
1、前端组件,用于处理来自客户端的传入请求
2、处理请求转换的中间管道
3、处理向上游服务发出的请求的后端组件
每个服务代理以自己的方式调用这些零件(入口点、服务器、虚拟主机、侦听器、过滤器、中间件、上游、端点等)调用这些部分,但 Traefik 人员甚至进一步进一步......
在以往的历史版本中,Traefik 基于入口点 - >前端 - >后端模型等链路处理规则模型,具体可参考如下示意图:
图片源自:Traefik V1.7 docs.
然而,在 2019 年,新的 Traefik 核心版本已经开始发布,引入了突破性的配置更改和改进策略,可参考如下示意图:
图片源自:Traefik V2 docs.
因此,在 Traefik 2 体系中,我们现在引入了路由器和服务,而不是前端和后端。还有一个明确的中间件组件层,用于处理额外的请求转换。嗯,咋一看,似乎很完美!但是,如果 V1 文档基本上是从体系结构概述开始的,那么进一步阅读就简单多了,那么在 V2 的情况下,我们需要深入到路由或中间件概念,以获得整个 Traefik 架构模型画像,基于此,我们才能够对其运用自如。
基于 Traefik 1.x 进行加权负载平衡
其实,从官方给予的相关文档可以看出,基于 Traefik 1.x 的灰度相对而言,还是较为简单。我们基于 Docker 提供商来运行 Traefik:v1.7 容器,其相关操作示例命令行如下所示:
[administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm \ --name traefik-v1.7 \ -p 9999:80 \ -v /var/run/docker.sock:/var/run/docker.sock \ traefik:v1.7 \ --docker \ --docker.exposedbydefault=false
由于它是 V1,我们需要在前端和后端进行思考。显然,每个容器都将成为特定后端的服务器。可以方便地使用 Traefik.weight 标签分配服务器的权重,具体如下所示:
# Run the current app version (weight 40) [administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm --name app_normal \ --label "traefik.enable=true" \ --label "traefik.backend=app_weighted" \ --label "traefik.frontend.rule=Host:example.local" \ --label "traefik.weight=40" \ nginx:1.19.1 # Run the contender version (weight 10) [administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm --name app_canary \ --label "traefik.enable=true" \ --label "traefik.backend=app_weighted" \ --label "traefik.frontend.rule=Host:example.local" \ --label "traefik.weight=10" \ nginx:1.19.2
我们通过命令行进行请求发送,来验证下其是否有效,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % for i in {1..100}; do curl -s -o /dev/null -D - -H Host:example.local localhost:9999 | grep Server; done | sort | uniq -c > 80 Server: nginx/1.19.1 > 20 Server: nginx/1.19.2
基于上述输出结果,我们可以看出:100 个请求中有 20 个已经由灰度发布的容器提供了服务。如果我们在某个时候不需要灰度服务,我们可以简单地将其停止。
现在,如果我们需要重复流量探测,100% 的请求将由 app_normal 容器提供,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % for i in {1..100}; do curl -s -o /dev/null -D - -H Host:example.local localhost:9999 | grep Server; done | sort | uniq -c > 100 Server: nginx/1.19.1
基于 Traefik 2.x 进行加权负载平衡
事情即将开始变得越来越复杂了。。。在研究了 V2 文档之后,我再也找不到 weight 相关指令了。接踵而来的便是“加权循环服务(WRR)“。
WRR 能够基于权重在多个服务之间进行负载平衡。
但它有几个局限性:
1、此策略仅适用于服务之间的负载平衡,而不适用于服务器之间的负载平衡。
2、此策略当前可通过文件或入口路由提供程序定义。
由于 WRR 这个功能目前仅支持 File Provider,所以我们需要开启该 Provider 才能使用,这里需要注意的是由于需要开启 File Provider,所以我们需要提供一个文件用于该 Provider 的配置,我们这里主要基于 Docker 中,当然,也可以用在 Kubernetes 集群中的,基于此场景,我们需要通过一个 ConfigMap 对象,将配置文件内容挂载到 Traefik 的 Pod 中去,然后通过将一个命名为 traefik-dynamic-conf 的 ConfigMap 对象挂载到 /config 目录下面去,接下来再通过 --providers.file.filename 参数指定配置文件来开启 File Provider,另外添加 - --providers.file.watch=true 参数可以让 Traefik 动态更新配置。其 YAML 文件如下所示:
...... volumes: - name: config configMap: name: traefik-dynamic-conf containers: - image: traefik:v2.0.2 name: traefik-ingress-lb volumeMounts: - name: config mountPath: /config ports: - name: web containerPort: 80 hostPort: 80 - name: admin containerPort: 8080 hostPort: 8080 args: - --entrypoints.web.Address=:80 - --api.insecure=true - --providers.file.watch=true - --providers.file.filename=/config/traefik-dynamic.toml - --api - --api.debug=true - --api.dashboard=true - --accesslog
完整的 YAML 可通过链接:https://github.com/cnych/kubeapp/tree/master/traefik2/canary 获取。
以 Docker 作为文件提供程序来定义 WRR 服务 ,其基本的流程如下所示:
1、首先,在文件中定义 WRR 服务:
[administrator@JavaLangOutOfMemory ~ ] % cat << "EOF" > file_provider.yml --- http: routers: router0: service: app_weighted rule: "Host(`example.local`)" services: app_weighted: weighted: services: - name: app_normal@docker # I'm not defined yet weight: 40 - name: app_canary@docker # Neither do it weight: 10 EOF
请注意,我们没有在那里定义任何服务器(即容器)。相反,我们根据其子服务( app_normal 和 app_canary )定义了 app_weighted(有@docker后缀表示这些服务预期由 Docker 提供者定义)。
2、基于 Docker 和文件提供程序启动 Traefik:v2.5 容器,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm --name traefik-v2.5 \ -p 9999:80 \ -v /var/run/docker.sock:/var/run/docker.sock \ -v `pwd`:/etc/traefik_providers \ traefik:v2.5 \ --providers.docker \ --providers.docker.exposedbydefault=false \ --providers.file.filename=/etc/traefik_providers/file_provider.yml
现在,是时候启动应用程序容器了。由于它是 V2,我们需要在配置容器标签时考虑路由器和服务,如下所示:
# Run the current app version (weight 40) [administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm --name app_normal_01 \ --label "traefik.enable=true" \ --label "traefik.http.services.app_normal.loadbalancer.server.port=80" \ --label "traefik.http.routers.app_normal_01.entrypoints=traefik" \ nginx:1.19.1 # Run the contender version (weight 10) [administrator@JavaLangOutOfMemory ~ ] % docker run -d --rm --name app_canary_01 \ --label "traefik.enable=true" \ --label "traefik.http.services.app_canary.loadbalancer.server.port=80" \ --label "traefik.http.routers.app_canary_01.entrypoints=traefik" \ nginx:1.19.2
让我们尝试理解这些标签背后的本质。通常,启动容器意味着创建单个容器服务。如果我们没有提出其他要求,Traefik 2 会使用容器的名称隐式地创建这样一个服务(出于某些原因,将 uu替换为 -)。除此之外,它还添加了一个路由规则主机(`<container name goes here>`)。
但在我们的例子中,我们不希望为容器提供任意服务。相反,我们确切地知道普通应用程序容器(app_normal)的服务名称和金丝雀应用程序容器(app_canary)的服务名称。因此,我们需要以某种方式将容器(即服务器)绑定到所需的服务。一种有点老套的方法是使用 traefik.http.services.<service name>.loadbalancer.server.port=80 标签。我们实际上不需要在这里指定端口,因为 Traefik 会自己找到它。但这样做可以让我们引入 app_normal 和 app_canary 服务,并将容器放在其中。
对于第二个标签,基于容器平台自动分配给每个容器的默认路由规则主机(`<container name goes here>`),为了避免这些容器意外暴露于集群外部,我们使用标签 traefik.http.routers.<stub>.entrypoints=traefik。
最后,我们来验证下其是否有效,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % for i in {1..100}; do curl -s -o /dev/null -D - -H Host:example.local localhost:9999 | grep Server; done | sort | uniq -c > 80 Server: nginx/1.19.1 > 20 Server: nginx/1.19.2
基于上述输出结果,我们可以看出:如同 V1 的期望结果一致。 如果我们需要停止灰度容器呢?其实其并不难,app_weighted@file 由于 app_canary 服务消失,服务将停止运行。很有可能,在 Traefik 生态中,甚至该文件也是一个动态提供者!首先,我们需要更新 app_weighted 服务,删除 app_canary 服务,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % cat << "EOF" > file_provider.yml --- http: routers: router0: service: app_weighted rule: "Host(`example.local`)" services: app_weighted: weighted: services: - name: app_normal@docker # I'm not defined yet weight: 40 # - name: app_canary@docker # Neither do it # weight: 10 EOF
Traefik 将自动获取更改(请注意,装载单个文件而不是其父文件夹将破坏 Traefik 的文件监视程序,它将永远不会注意到更改)。一旦应用了更改,我们可以安全地停止灰度容器。再次进行请求验证,其结果如下所示:
[administrator@JavaLangOutOfMemory ~ ] % for i in {1..100}; do curl -s -o /dev/null -D - -H Host:example.local localhost:9999 | grep Server; done | sort | uniq -c > 100 Server: nginx/1.19.1
基于上述所述,所有的一切技术应用都是从官方文档开始,因此,只有熟悉官网相关的原理及所提供的 Demo 实践操作,后续的技术探索之路才能走得更远、更踏实。
# 参考资料