Kubernetes 中如何实现灰度发布

当你Kubernetes 集群中部署业务时,可以利用 Kubernetes 原生提供的灰度发布的方式去上线业务。这种方式是通过在旧版本和新版本的服务之间,定义一个差异化的 Label,根据不同版本之间的公共 Label 负载流量到后端 Pod,最终实现根据 Pod 的副本数控制流量的百分比。

如下图所示:用户定义了两个 Deployment 对象,其中旧版本名为 frontend-stable,有3个副本。新版本为 frontend-canary,有1个副本。此时定义了一个 Service 对象,使用它们之间公共的 Label 进行选择。这就使得用户访问 frontend 这个 Service 时,能以 3:1 的比例同时访问到两个版本。并且还可以通过调整副本数持续控制流量比例,最终达到完整上线。

Kubernetes 默认的实现方式在简单的部署场景下很有效,但是在一些复杂场景中,仍然会有较大的局限,如:

  1. 业务配置自动伸缩后,会直接影响灰度发布的流量比例
  2. 低百分比的流量控制占用资源高,如 1 % 的流量到达新版本,则至少需要 100 个副本
  3. 精确的流量分发控制,使访问到新版本中的用户一直是同一批,而不是某个用户访问时随机切换

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:
# CI_COMMIT_REF_NAME为分支名称
name: testbbb-${CI_COMMIT_REF_NAME}
namespace: test-istio
labels:
app: testbbb
# 对应加一个version label
version: ${CI_COMMIT_REF_NAME}
spec:
replicas: 2
selector:
matchLabels:
app: testbbb
# 对应加一个version label
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 label
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
# while [ true ];do curl "http://testbbb.test-istio:32000/sapi/test/get" -w '\n';sleep 1;done
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
# while [ true ];do curl "http://testbbb.test-istio:32000/sapi/test/get" -w '\n';sleep 1;done
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