在日常的项目开发过程中时,我们时常会面临服务变更的挑战,为不影响用户体验,我们往往尽可能需要规避服务不可用风险。因此,持续交付便应运而生,其被接受为一种企业软件实践,并且是完善的持续集成原则的自然演变。然而,持续部署仍然非常罕见,这可能是由于管理的复杂性以及担心部署失败会影响系统可用性。在整个持续交付体系中,金丝雀发布,或许是最为经典的一个场景,基于此,我们能够很快发现不健康和“有问题”的服务,并且可以毫不费力地回滚到上一个的版本。
金丝雀部署
什么是金丝雀部署?也称“灰度部署”,通常来讲,在原有版本可用的情况下,同时部署一个新版本应用作为“金丝雀”,测试新版本的性能和表现,在保障整体系统稳定的前提下,尽早发现、及时调整。
金丝雀部署,其并非黑即白的部署方式,它能够缓慢的将特定百分比的流量引导至一小部分用户,若验证没有问题后,再推广到全部用户,并逐步淘汰旧版本,以降低生产环境引入新功能带来的风险。针对金丝雀部署工作流,其往往主要涉及以下阶段:
Step 1:将流量从待部署节点移出,更新该节点服务至待发布状态,此时,节点称为“金丝雀节点”。
Step 2:依据不同的场景策略,将流量引入至金丝雀节点。流量引入策略可以依据实际的业务场景情况而定,例如,随机样本策略(随机引入)、狗粮策略(就是内部用户或员工先尝鲜)、区域策略(不同区域用户使用不同版本)、用户特征策略(这种比较复杂,需要根据用户个人资料和特征进行分流)。
Step 3:金丝雀节点验证通过后,选取更多的节点作为金丝雀节点,然后重复步骤一和步骤二,直到所有节点全部更新至最新状态。
在本篇文章中,笔者将选择 Traefik 与 Flagger 相结合使用,以探索应用程序/服务在发布新版本时所拥有的一些潜在可能性。
我们先来介绍下 Flagger ,Flagger 是一个云原生计算基金会项目,是 GitOps 工具 Flux 系列的一部分。作为一种渐进式交付工具,Flagger 可自动执行在 Kubernetes 上运行的应用程序的发布过程。它通过在衡量指标和运行一致性测试的同时逐渐将流量转移到新版本来降低在生产中引入新软件版本的风险。
Flagger 可以针对以下部署策略运行自动化的应用程序分析、升级和回滚:
- Canary(渐进式流量转移)
- A/B 测试(HTTP 标头和 cookie 流量路由)
- 蓝/绿(流量开关或镜像)
对于 Canary 部署和 A/B 测试,我们需要一个第 7 层流量管理解决方案,例如服务网格(Istio、Linkerd、App Mesh)或入口控制器(Contour、NGINX、Gloo)。而对于蓝/绿部署,则不需要服务网格或入口控制器。
Flagger 使用服务网格(App Mesh、Istio、Linkerd、Open Service Mesh)或入口控制器(Contour、Gloo、NGINX、Skipper、Traefik)实现了多种部署策略(Canary 发布、A/B 测试、蓝/绿镜像) 用于流量路由。对于发布分析,Flagger 可以查询 Prometheus、Datadog、New Relic、CloudWatch 或 Graphite,并使用 Slack、MS Teams、Discord 和 Rocket 进行警报。
Flagger 可以使用 Kubernetes 自定义资源进行配置,并且兼容任何为 Kubernetes 制作的 CI/CD 解决方案。由于 Flagger 是声明性的并对 Kubernetes 事件做出反应,因此它可以与 Flux、JenkinsX、Carvel、Argo 等工具一起用于 GitOps 管道中。
除此之外,Flagger 同时也会跟踪 Kubernetes 部署引用的 ConfigMap 和 Secrets,并在这些对象中的任何一个发生更改时触发金丝雀分析。在生产中提升工作负载时,代码(容器映像)和配置(配置映射和机密)都会同步。
前置条件:Flagger 需要依赖 Kubernetes 集群 v1.16 或更高版本以及 Traefik v2.3 或更高版本。
接下来,我们用 Helm (此处为 v3 版本)部署 Traefik ,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ] % helm repo add traefik https://helm.traefik.io/traefik [administrator@JavaLangOutOfMemory ~ ] % kubectl create ns traefik [administrator@JavaLangOutOfMemory ~ ] % cat <<EOF | helm upgrade -i traefik traefik/traefik --namespace traefik -f - deployment: podAnnotations: prometheus.io/port: "9100" prometheus.io/scrape: "true" prometheus.io/path: "/metrics" metrics: prometheus: entryPoint: metrics EOF
在与 Traefik 相同的命名空间中安装 Flagger 和 Prometheus 等支撑组件,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ]% helm repo add flagger https://flagger.app [administrator@JavaLangOutOfMemory ~ ]% helm upgrade -i flagger flagger/flagger \ --namespace traefik \ --set prometheus.install=true \ --set meshProvider=traefik
Flagger 采用 Kubernetes 部署和可选的水平 Pod 自动缩放器 (HPA),然后创建一系列对象(Kubernetes 部署、ClusterIP 服务和 TraefikService)。这些对象将应用程序暴露在集群之外并驱动金丝雀分析和推广。
接下来,我们来创建一个测试使用的命名空间,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl create ns test [administrator@JavaLangOutOfMemory ~ ]% kubectl apply -k https://github.com/fluxcd/flagger//kustomize/podinfo?ref=main
然后,部署负载测试服务以在金丝雀分析期间生成流量:
[administrator@JavaLangOutOfMemory ~ ]% helm upgrade -i flagger-loadtester flagger/loadtester \ --namespace=test
创建引用由 Flagger 生成的 TraefikService 的 Traefik IngressRoute(将 app.example.com 替换为自己的域),其 Demo 文件如下所示:
apiVersion: traefik.containo.us/v1alpha1 kind: IngressRoute metadata: name: podinfo namespace: test spec: entryPoints: - web routes: - match: Host(`app.example.com`) kind: Rule services: - name: podinfo kind: TraefikService port: 80
将上述资源另存为 podinfo-ingressroute.yaml 然后执行资源配置操作,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl apply -f ./podinfo-ingressroute.yaml
此处,我们创建金丝雀自定义资源(将 app.example.com 替换为自己的域),具体如下所示:
apiVersion: flagger.app/v1beta1 kind: Canary metadata: name: podinfo namespace: test spec: provider: traefik # deployment reference targetRef: apiVersion: apps/v1 kind: Deployment name: podinfo # HPA reference (optional) autoscalerRef: apiVersion: autoscaling/v2beta2 kind: HorizontalPodAutoscaler name: podinfo # the maximum time in seconds for the canary deployment # to make progress before it is rollback (default 600s) progressDeadlineSeconds: 60 service: # ClusterIP port number port: 80 # container port number or name targetPort: 9898 analysis: # schedule interval (default 60s) interval: 10s # max number of failed metric checks before rollback threshold: 10 # max traffic percentage routed to canary # percentage (0-100) maxWeight: 50 # canary increment step # percentage (0-100) stepWeight: 5 # Traefik Prometheus checks metrics: - name: request-success-rate interval: 1m # minimum req success rate (non 5xx responses) # percentage (0-100) thresholdRange: min: 99 - name: request-duration interval: 1m # maximum req duration P99 # milliseconds thresholdRange: max: 500 webhooks: - name: acceptance-test type: pre-rollout url: http://flagger-loadtester.test/ timeout: 10s metadata: type: bash cmd: "curl -sd 'test' http://podinfo-canary.test/token | grep token" - name: load-test type: rollout url: http://flagger-loadtester.test/ timeout: 5s metadata: type: cmd cmd: "hey -z 10m -q 10 -c 2 -host app.example.com http://traefik.traefik" logCmdOutput: "true"
[administrator@JavaLangOutOfMemory~ ]% kubectl apply -f ./podinfo-canary.yaml
几秒钟后,Flager 将创建金丝雀对象:
# applied deployment.apps/podinfo horizontalpodautoscaler.autoscaling/podinfo canary.flagger.app/podinfo # generated deployment.apps/podinfo-primary horizontalpodautoscaler.autoscaling/podinfo-primary service/podinfo service/podinfo-canary service/podinfo-primary traefikservice.traefik.containo.us/podinfo
Flagger 实施了一个控制循环,在测量 HTTP 请求成功率、请求平均持续时间和 Pod 健康状况等关键性能指标的同时,逐渐将流量转移至金丝雀。根据对相关指标的分析,发布或中止金丝雀部署,并将分析结果发布到相关平台。
通过更新容器镜像触发金丝雀部署,具体操作方法如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n test set image deployment/podinfo \ podinfod=stefanprodan/podinfo:4.0.6
Flagger 检测到部署内容已更改并开始新的部署:
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n test describe canary/podinfo Status: Canary Weight: 0 Failed Checks: 0 Phase: Succeeded Events: New revision detected! Scaling up podinfo.test Waiting for podinfo.test rollout to finish: 0 of 1 updated replicas are available Pre-rollout check acceptance-test passed Advance podinfo.test canary weight 5 Advance podinfo.test canary weight 10 Advance podinfo.test canary weight 15 Advance podinfo.test canary weight 20 Advance podinfo.test canary weight 25 Advance podinfo.test canary weight 30 Advance podinfo.test canary weight 35 Advance podinfo.test canary weight 40 Advance podinfo.test canary weight 45 Advance podinfo.test canary weight 50 Copying podinfo.test template spec to podinfo-primary.test Waiting for podinfo-primary.test rollout to finish: 1 of 2 updated replicas are available Routing all traffic to primary Promotion completed! Scaling down podinfo.test
备注:如果在 Canary 分析期间对部署应用新更改,Flager 将重新启动分析。
我们可以通过以下方式监控所有金丝雀部署信息,具体如下所示:
[administrator@JavaLangOutOfMemory ~ ]% watch kubectl get canaries --all-namespaces NAMESPACE NAME STATUS WEIGHT LASTTRANSITIONTIME test podinfo-2 Progressing 30 2020-08-14T12:32:12Z test podinfo Succeeded 0 2020-08-14T11:23:88Z
上述我们简要介绍了金丝雀发布的相关理论基础,接下来,我们来看一下基于此场景下的自动回滚。
在金丝雀分析过程中,我们可以生成 HTTP 500 错误来测试 Flagger 是否暂停并回滚有故障的版本。
此时,我们进行另一个金丝雀部署,具体操作如下:
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n test set image deployment/podinfo \ podinfod=stefanprodan/podinfo:4.0.6
进入 Pod 内,执行相关命令,具体操作如下:
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n test exec -it deploy/flagger-loadtester bash
[administrator@JavaLangOutOfMemory ~ ]% hey -z 1m -c 5 -q 5 http://app.example.com/status/500
[administrator@JavaLangOutOfMemory ~ ]% watch -n 1 curl http://app.example.com/delay/1
当失败的检查次数达到金丝雀分析所设定的阈值时,流量将路由回主节点,金丝雀缩放为零,并将推出标记为失败。
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n traefik logs deploy/flagger -f | jq .msg New revision detected! Scaling up podinfo.test Canary deployment podinfo.test not ready: waiting for rollout to finish: 0 of 1 updated replicas are available Starting canary analysis for podinfo.test Pre-rollout check acceptance-test passed Advance podinfo.test canary weight 5 Advance podinfo.test canary weight 10 Advance podinfo.test canary weight 15 Advance podinfo.test canary weight 20 Halt podinfo.test advancement success rate 53.42% < 99% Halt podinfo.test advancement success rate 53.19% < 99% Halt podinfo.test advancement success rate 48.05% < 99% Rolling back podinfo.test failed checks threshold reached 3 Canary failed! Scaling down podinfo.test
Canary 分析可以通过 Prometheus 查询进行扩展。我们创建指标模板并将其应用于集群,具体如下所示:
apiVersion: flagger.app/v1beta1 kind: MetricTemplate metadata: name: not-found-percentage namespace: test spec: provider: type: prometheus address: http://flagger-prometheus.traefik:9090 query: | sum( rate( traefik_service_request_duration_seconds_bucket{ service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd", code!="404", }[{{ interval }}] ) ) / sum( rate( traefik_service_request_duration_seconds_bucket{ service=~"{{ namespace }}-{{ target }}-canary-[0-9a-zA-Z-]+@kubernetescrd", }[{{ interval }}] ) ) * 100
编辑金丝雀分析并添加未找到错误率检查:
analysis: metrics: - name: "404s percentage" templateRef: name: not-found-percentage thresholdRange: max: 5 interval: 1m
上述配置通过检查 HTTP 404 req/sec 百分比是否低于总流量的 5% 来验证金丝雀。如果 404s 率达到 5% 阈值,则金丝雀失败。通过更新容器镜像触发金丝雀部署,如下所示:
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n test set image deployment/podinfo \ podinfod=stefanprodan/podinfo:4.0.6
生成 HTTP 500 错误及产生延迟相关命令如下所示:
[administrator@JavaLangOutOfMemory ~ ]%hey -z 1m -c 5 -q 5 http://app.example.com/status/500
[administrator@JavaLangOutOfMemory ~ ]% watch curl http://app.example.com/status/400
[administrator@JavaLangOutOfMemory ~ ]% kubectl -n traefik logs deployment/flagger -f | jq .msg Starting canary deployment for podinfo.test Advance podinfo.test canary weight 5 Advance podinfo.test canary weight 10 Advance podinfo.test canary weight 15 Halt podinfo.test advancement 404s percentage 6.20 > 5 Halt podinfo.test advancement 404s percentage 6.45 > 5 Halt podinfo.test advancement 404s percentage 7.60 > 5 Halt podinfo.test advancement 404s percentage 8.69 > 5 Halt podinfo.test advancement 404s percentage 9.70 > 5 Rolling back podinfo.test failed checks threshold reached 5 Canary failed! Scaling down podinfo.test
如果配置了告警功能,Flagger 将会发送一条通知,描述金丝雀失败的具体原因。以上为相关内容详情,针对 Flagger 技术的深入分析,大家有兴趣的话,可阅读 Flagger 官方文档以获得更为详尽的答案。
# 参考资料