1. 场景简介
灰度发布是降低生产部署风险,提升线上服务稳定性的重要手段,这在当前快速迭代的软件研发中尤为重要。相对于 k8s 默认的滚动部署或者简单的 Pod 分批,基于流量特征的灰度发布验证更精准,风险更低。
在云原生场景下,基于 nginx ingress 的灰度发布是被广泛使用的方案之一。该方案通过在流量入口侧进行灰度和正常流量的路由调配,将灰度流量导入到灰度的服务版本,从而可以在全量部署到生产环境前,对新版本进行验证,当验证不通过时,可以及时回退,将风险控制在较低的范围内。而整个发布和验证,能做到对终端用户服务的不间断和体验一致。
2. 基本原理
对于常见的 web 服务,ingress 灰度发布的基本逻辑是:将指向同一入口(HOST)的访问请求,根据特征或流量比例,将一部分流量路由到灰度服务中,通过对灰度流量的监控和验证,判断灰度服务是否符合上线标准。
按照流量的路径,我们可以画出 ingress 流量灰度在 k8s 上的简单架构(如下图)。
流量从入口的 HOST 进来后,会根据 nginx-ingress 的流量标识(定义在 ingress 的 annotation 中),将符合灰度标识的请求路由到灰度环境的 service 中,进而进入背后的工作负载 Pod。对于每个环境内部的 service 和 deployment,它们并不主动感知灰度标识,也就是说,通常情况下,内部的 rpc 流量并不携带灰度标识。因此,这种架构主要解决的是外部流量灰度的问题,适合服务数量不太多,服务间调用比较简单的场景。
3. 具体实践
接下来,我们结合云效 Appstack,来看下如何在阿里云 ACK 集群上进行应用的 ingress 灰度发布。
我们首先导入 ACK 集群并创建应用。
3.1. 定义和管理灰度环境
3.1.1. 定义环境
在应用中,我们分别定义灰度环境与生产环境,两个环境共享同一个 k8s 集群,我们将灰度环境命名为 grey,生产环境命名为 ack-prod。
3.1.2. 定义部署编排
我们打开应用设置,编排配置来定义部署编排。灰度发布生效的关键是 ingress 中的 canary 注解,需要查看你的 nginx-ingress-controller 版本,以确定可以支持的注解。我测试的集群上 nginx-ingress-controller 的版本是 0.30,采用 header 来标识灰度流量。
为了做到只在灰度环境中开启灰度的路由配置,我们使用了编排模板的条件语句,见下面的编排示例:
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ .AppStack.appName }}-{{ .AppStack.envName }}
namespace: {{ .Values.namespace }}
{{ if eq .AppStack.envName "grey" }}
annotations:
# 开启Canary。
nginx.ingress.kubernetes.io/canary: "true"
# 请求头为_env。
nginx.ingress.kubernetes.io/canary-by-header: "_env"
# 请求头_env的值为grey时,请求才会被路由到新版本服务中。
nginx.ingress.kubernetes.io/canary-by-header-value: "grey"
{{ end }}
spec:
rules:
- host: {{ .Values.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .AppStack.appName }}-{{ .AppStack.envName }}
port:
number: 80
可以看到在 ingress 的编排中,仅名称为“grey”的环境,会开启 canary 的 annotaion,并且将灰度标识设置为通过 _env: grey 的 header 标签。
同时在 ingress 的 rules 配置中,通过变量 host 来指定路由关联的 HOST,这里 ACK 灰度环境和 ACK 生产环境分别关联灰度环境变量组和生产环境变量组,这两个变量组的 host 变量采用相同的值。
示例:
#
namespace = demo-pre
#
host = go.demo.prod
#
namespace = demo-prod
#
host = go.demo.prod
除了 ingress 外,还需要配置对应的 service 和 deployment。
---
apiVersion: v1
kind: Service
metadata:
name: {{ .AppStack.appName }}-{{ .AppStack.envName }}
namespace: {{ .Values.namespace }}
spec:
selector:
run: {{ .AppStack.appName }}-{{ .AppStack.envName }}
ports:
- protocol: TCP
port: 80
targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .AppStack.appName }}-{{ .AppStack.envName }}
labels:
run: {{ .AppStack.appName }}-{{ .AppStack.envName }}
namespace: {{ .Values.namespace }}
spec:
replicas: {{ .Values.replicas }}
selector:
matchLabels:
run: {{ .AppStack.appName }}-{{ .AppStack.envName }}
template:
metadata:
labels:
run: {{ .AppStack.appName }}-{{ .AppStack.envName }}
spec:
containers:
- name: main
image: {{ .AppStack.image.backend }}
ports:
- containerPort: 8080
resources:
limits:
cpu: {{ .Values.cpuLimit }}
memory: {{ .Values.memoryLimit }}
requests:
cpu: {{ .Values.cpuRequest }}
memory: {{ .Values.memoryRequest }}
3.2. 准备示例代码并关联到应用中
可从 atomgit 上下载实例代码,路径为:https://atomgit.com/feiyuw/demo-go-echo.git
将代码导入到云效代码管理 Codeup,然后关联到应用中。
3.3. 定义灰度发布的流程
接下来,我们通过云效 Appstack 的研发流程,定义灰度发布的整个过程,见下图:
在该流程中,每次执行,我们都会从 master 分支拉取代码进行镜像构建,构建完成后会转交运维进行审批,审批通过后即开始部署过程。
部署过程包含灰度部署、灰度验证、生产部署和灰度清理四个步骤。
灰度部署步骤会将前面构建出来的镜像更新到 ACK 灰度环境,此时通过在请求中携带 _env:grey 的 header 就可进行灰度验证。
灰度验证是一个人工卡点,用于对灰度环境的观测和验证,如果验证通过即自动进行生产环境的部署,如果验证失败,则跳过生产部署,执行灰度清理
生产部署步骤会将镜像更新到 ACK 生产环境,此时新的服务版本将对普通用户可见。注意,这里为了降低风险,生产部署的策略被设置为了分批,且首批暂停的模式,保证线上仍然是逐步放量的,且有机会快速回退。
灰度清理步骤主要是清理灰度环境的资源,一方面节约资源,另一方面避免灰度验证不通过时对线上的影响。
在 云效 Appstack 上相关应用的设置中,进行研发流程设置(如果仅是体验灰度发布,可仅配置生产阶段)。
上述流程可以参考下面的流水线 yaml 来定义,请注意将其中的acr_docker_build_step 步骤的镜像地址和服务连接修改为正确的值,并把grey_validate 和 ops_validate 包含的 userId 替换为自己的阿里云的账号 ID。
---
stages:
build:
name: "构建"
jobs:
go_build:
name: "Go 镜像构建"
steps:
golang_build_step:
name: "Golang 构建"
step: "GolangBuild"
with:
goVersion: "1.20.x"
run: |
export GOPROXY=https://goproxy.cn
make build
upload_step:
step: "ArtifactUpload"
name: "构建物上传"
with:
serviceConnection: "wtdbdh89rrfdsod6"
repo: "flow_generic_repo"
artifact: "demo-go-echo"
version: "prod-${CI_COMMIT_ID}.${DATETIME}"
filePath:
- "demo-go-echo"
- "deploy.sh"
acr_docker_build_step:
name: "镜像构建并推送至阿里云镜像仓库个人版"
step: "ACRDockerBuild"
with:
artifact: "image"
dockerfilePath: "Dockerfile"
dockerRegistry: "registry.cn-zhangjiakou.aliyuncs.com/docker007/demo-go-echo"
dockerTag: "prod-${CI_COMMIT_ID}.${DATETIME}"
region: "cn-zhangjiakou"
serviceConnection: "<connectionId>"
approve:
name: "部署审核"
jobs:
ops_validate:
name: "运维审批"
component: "ManualValidate"
with:
validatorType: "users"
validators:
- <userId>
grey:
name: "灰度验证"
jobs:
grey_deploy_job:
name: "ACK灰度部署"
component: "AppStackFlowDeploy"
with:
application: "demo-go-echo"
environment: "grey"
artifacts:
- label: "backend"
value: "$[stages.build.go_build.acr_docker_build_step.artifacts.image.dockerUrl]"
autoDeploy: true
grey_validate:
name: "灰度验证"
component: "ManualValidate"
needs:
- "grey_deploy_job"
with:
validatorType: "users"
validators:
- <userId>
deploy:
name: "部署"
jobs:
ack_deploy_job:
name: "ACK生产部署"
component: "AppStackFlowDeploy"
condition: |
succeed('grey.grey_validate')
with:
application: "demo-go-echo"
environment: "ack-prod"
artifacts:
- label: "backend"
value: "$[stages.build.go_build.acr_docker_build_step.artifacts.image.dockerUrl]"
autoDeploy: true
cleanup:
name: "清理环境"
jobs:
cleanup_grey_env_job:
name: "清理灰度环境"
component: "AppStackCleanEnv"
needs:
- "grey.grey_validate"
- "deploy.ack_deploy_job"
condition: |
failed('grey.grey_validate') || succeed('deploy.ack_deploy_job')
with:
application: "demo-go-echo"
environment: "grey"
deleteEnv: "cleanEnv"
3.4. 验证灰度发布流程
我们假设研发流程仅包含生产阶段,先运行一次生产阶段,将应用部署到灰度和生产环境中,注意:环境第一次部署可能需要手动创建部署单。
修改代码,将 routes.go 里面的版本号修改为新的值,再次执行生产阶段,直到灰度验证步骤,此时生产环境与灰度环境运行不同的版本。
通过 kubectl get ing -A 获取 ingress 的出口 IP,在本地/etc/hosts 中将其绑定到 go.demo.prod 中,如:
127.0.0.1 go.demo.prod # 将127.0.0.1修改为正确的出口IP
打开终端,通过 httpie 或者 curl 请求/version 接口,以 httpie 为例,请求命令为:
http -v http://go.demo.prod/version # 请求正式环境
http -v http://go.demo.prod/version _env:grey # 请求灰度环境
4. 常见问题
4.1. 有了灰度环境,生产环境部署的时候还需要分批吗?
建议在生产环境部署的时候仍然开启分批,因为灰度环境虽然验证通过了,但受到数据量等影响,生产仍然不建议全量一起上,通过分批,可以把风险控制在小范围内,避免大的故障的发生。
4.2. 如何在研发流程上整合配置变更和数据变更?
可以将配置变更和数据变更作为研发阶段的步骤,编排到研发流程的流水线 yaml 中,通常建议在应用部署前执行数据变更,部署后执行配置变更。同时,对应于 k8s 上的灰度环境,如果采用了类似 Nacos 这样的配置中心,也应当由对应的灰度 namespace,从而避免直接修改生产配置。
4.3. 灰度环境包含多个应用,如何保证其内部服务间调用也是走的灰度环境?
完整的方案建议参考 MSE 等产品。
如果应用数量比较少,链路比较简单,且接受基于 k8s 的简单方案,可以为每个应用都定义一个灰度环境,共享同一个集群和 namespace,应用间通过 service 进行调用。此时,由于 namespace 的隔离,灰度环境内的应用互相调用只会调用同 namespace 下的其它应用的灰度版本。
这种方案需要保证各应用灰度环境的长期可用,因此研发流程最后的清理环境步骤需要被移除。
4.4. 如何在流程中关联其它类型的发布如函数计算?
可以在函数计算的相关步骤编排到研发流程的流水线 yaml 中,从而实现双方的联动。