距离上一次更新该文章已经过了 461 天,文章所描述的內容可能已经发生变化,请留意。
Kubernetes 中如何实现灰度发布
当你Kubernetes 集群中部署业务时,可以利用 Kubernetes 原生提供的灰度发布的方式去上线业务。这种方式是通过在旧版本和新版本的服务之间,定义一个差异化的 Label,根据不同版本之间的公共 Label 负载流量到后端 Pod,最终实现根据 Pod 的副本数控制流量的百分比。
如下图所示:用户定义了两个 Deployment 对象,其中旧版本名为 frontend-stable,有3个副本。新版本为 frontend-canary,有1个副本。此时定义了一个 Service 对象,使用它们之间公共的 Label 进行选择。这就使得用户访问 frontend 这个 Service 时,能以 3:1 的比例同时访问到两个版本。并且还可以通过调整副本数持续控制流量比例,最终达到完整上线。

Kubernetes 默认的实现方式在简单的部署场景下很有效,但是在一些复杂场景中,仍然会有较大的局限,如:
- 业务配置自动伸缩后,会直接影响灰度发布的流量比例
- 低百分比的流量控制占用资源高,如 1 % 的流量到达新版本,则至少需要 100 个副本
- 精确的流量分发控制,使访问到新版本中的用户一直是同一批,而不是某个用户访问时随机切换
Istio实现灰度发布
由于 Kubernetes 提供的灰度发布方式的局限性,在一些复杂场景下,我们就需要使用 Istio 来实现更精细的灰度发布策略。
简述
如下图所示,以 istio 官网提供的 Bookinfo 示例程序为例,给出了 virtual services 和 destination rules 的主要定义。其中 virtual services 主要分为两块,主机名和路由规则。主机名是客户端向服务发送请求时使用的一个或多个地址。当请求到达 virtual services 时,则会根据其定义的路由规则匹配。图中就定义了邮箱以 gmail.com 结尾的用户流量只会到达 v3 版本的实例上。而其他用户则以 1:9 的比例分别访问到 v1 和 v2 版本的服务。这种方式实现了精确的流量分发控制。
当用户流量来到 reviews.demo.svc.cluster.local 这个 Service 上时,可以看到 destination rules 的规则定义中根据 version 这个 label 定义了不同的实例集,实现了流量比例与副本数的解耦。不管 reviews-v1 有多少实例。始终只有 10% 的流量到达 destination rules 的 v1 子集中。这就解决了业务副本数与流量比例的冲突问题,也使得资源使用更加合理。

原理
Istio采用sidecar对应用流量进行了转发,通过Pilot下发路由规则,可以在不修改应用程序的前提下实现应用的灰度发布。
采用Istio后,可以通过定制路由规则将特定的流量(如指定特征的用户)导入新版本服务中,在生产环境下进行测试,同时通过渐进受控地导入生产流量,可以最小化升级中出现的故障对用户的影响。并且在同时存在新老版本服务时,还可根据应用压力对不同版本的服务进行独立的缩扩容,非常灵活。采用Istio进行灰度发布的流程如下图所示:

操作
部署testbbb服务 - Deployment&Service
旧服务版本为release-v1,新服务版本为release-v2,代码接口测试,返回环境变量PROJECT_VERSION,PROJECT_VERSION对应着当前服务的git版本
1 2 3 4
| router.get('/get', async (ctx) => { console.log('版本: ', process.env.PROJECT_VERSION); ctx.body = process.env.PROJECT_VERSION; });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| apiVersion: v1 kind: Service metadata: name: testbbb namespace: test-istio spec: selector: app: testbbb ports: - port: 32000 targetPort: 32000 appProtocol: HTTP type: ClusterIP --- apiVersion: apps/v1 kind: Deployment metadata: name: testbbb-${CI_COMMIT_REF_NAME} namespace: test-istio labels: app: testbbb version: ${CI_COMMIT_REF_NAME} spec: replicas: 2 selector: matchLabels: app: testbbb version: ${CI_COMMIT_REF_NAME} strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 0 maxSurge: 1 template: metadata: annotations: sidecar.istio.io/inject: 'true' labels: app: testbbb version: ${CI_COMMIT_REF_NAME} spec: containers: - image: $REGISTRY_ADDRESS/${NODE_ENV}/${CI_PROJECT_NAME}:v${CI_PIPELINE_ID} env: - name: NODE_ENV value: development - name: PROJECT_VERSION value: ${CI_COMMIT_REF_NAME} imagePullPolicy: IfNotPresent livenessProbe: tcpSocket: port: 32000 readinessProbe: tcpSocket: port: 32000 name: testbbb ports: - containerPort: 32000 dnsPolicy: ClusterFirst restartPolicy: Always
|
配置istio - Dr&Vs
备注:本例只是描述原理,因此为简单起见,将10%流量导入V2版本,在实际操作中,更可能是先导入较少流量,然后根据监控的新版本运行情况将流量逐渐导入,如采用5%,10%,20%,50% …的比例逐渐导入。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: testbbb namespace: test-istio spec: host: testbbb.test-istio.svc.cluster.local subsets: - labels: version: release-v1 name: release-v1 - labels: version: release-v2 name: release-v2 --- apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: testbbb namespace: test-istio spec: hosts: - testbbb.test-istio.svc.cluster.local http: - name: testbbb route: - destination: host: testbbb.test-istio.svc.cluster.local subset: release-v1 weight: 90 - destination: host: testbbb.test-istio.svc.cluster.local subset: release-v2 weight: 10
|

按9:1的比例将流量分发给v1和v2
我以为默认是轮询访问,但效果看好像并不是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v2 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v1 release-v2 release-v2 release-v1 release-v1 release-v1 release-v2 release-v1 release-v1 release-v1 release-v2 release-v1 release-v1
|
将所有流量导入到到V2版本的服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: testbbb namespace: test-istio spec: hosts: - testbbb.test-istio.svc.cluster.local http: - name: testbbb route: - destination: host: testbbb.test-istio.svc.cluster.local subset: release-v2
|

再次进行测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2 release-v2
|