基于Nginx Ingress实现灰度发布

什么是灰度发布和蓝绿发布?

简单来说,就是不要一次性把所有流量都切到新版本,而是先让一小部分用户试用新版本,观察一段时间没问题了再逐步放量。

  • 灰度发布:按照一定规则(比如特定用户、特定地区、或者按百分比)把部分流量导到新版本
  • 蓝绿发布:其实是灰度发布的一种特殊形式,通过权重控制流量分配,比如先给新版本10%流量,稳定后再调到50%,最后100%。与灰度发布不同的是,蓝绿发布通常是指同时存在两个完整的生产环境(蓝色和绿色),通过切换流量来实现无缝发布。

这样做的好处很明显:

  • 新版本有问题可以快速回滚,影响范围小
  • 可以在真实环境中验证新功能
  • 用户无感知,体验更好

Nginx Ingress的Canary机制

Nginx Ingress Controller提供了一套Canary(金丝雀)机制来实现灰度发布,主要通过几个annotation来控制:

  • nginx.ingress.kubernetes.io/canary: "true" - 开启Canary模式
  • nginx.ingress.kubernetes.io/canary-by-header - 基于请求头匹配
  • nginx.ingress.kubernetes.io/canary-by-header-value - 请求头的具体值
  • nginx.ingress.kubernetes.io/canary-by-cookie - 基于cookie匹配
  • nginx.ingress.kubernetes.io/canary-weight - 基于权重分配流量(0-100)

这几种方式的优先级是:canary-by-header > canary-by-cookie > canary-weight

需要注意的是,一个Ingress规则只能对应一个Canary Ingress,配置多个的话只有第一个会生效。另外,Canary Ingress必须与普通Ingress具有相同的host和path规则才能生效。

灰度发布流程图

我画了一张图展示了基于Nginx Ingress实现灰度发布的完整流程:

灰度发布流程图

从图中可以看出,灰度发布主要包括以下几个关键步骤:

  1. 部署老版本服务并创建普通Ingress规则
  2. 部署新版本服务
  3. 创建Canary Ingress规则,配置灰度策略(基于请求头、cookie或权重)
  4. 逐步调整流量分配比例
  5. 完全切换到新版本并清理资源

实战

下面我们用一个实际例子来演示整个流程。假设我们有一个Nginx服务要从老版本升级到新版本。

第一步:部署老版本服务

首先把现有的老版本服务部署起来,这个比较简单。

创建 old-nginx-deployment-and-service.yaml

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: old-nginx
spec:
replicas: 2
selector:
matchLabels:
run: old-nginx
template:
metadata:
labels:
run: old-nginx
spec:
containers:
- image: my-nginx:old # 老版本镜像
imagePullPolicy: IfNotPresent
name: old-nginx
ports:
- containerPort: 80
protocol: TCP
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: old-nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: old-nginx # 指向老版本Deployment
sessionAffinity: None
type: NodePort

然后创建Ingress规则 ingress.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gray-release
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- path: /
backend:
service:
name: old-nginx # 指向老版本服务
port:
number: 80
pathType: ImplementationSpecific

执行部署命令:

1
2
kubectl apply -f old-nginx-deployment-and-service.yaml
kubectl apply -f ingress.yaml

测试一下是否正常:

1
2
3
4
5
# 获取Ingress的外部IP
kubectl get ingress

# 访问服务
curl -H "Host: www.example.com" http://<EXTERNAL_IP>

如果能看到nginx的欢迎页面,说明老版本服务已经正常运行了。

第二步:部署新版本服务

现在我们要上线新版本了,先把新版本的服务部署起来。

创建 new-nginx-deployment-and-service.yaml

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: new-nginx
spec:
replicas: 1
selector:
matchLabels:
run: new-nginx
template:
metadata:
labels:
run: new-nginx
spec:
containers:
- image: my-nginx:new # 新版本镜像
imagePullPolicy: IfNotPresent
name: new-nginx
ports:
- containerPort: 80
protocol: TCP
restartPolicy: Always
---
apiVersion: v1
kind: Service
metadata:
name: new-nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: new-nginx # 指向新版本Deployment
sessionAffinity: None
type: NodePort

部署新版本:

1
kubectl apply -f new-nginx-deployment-and-service.yaml

这时候新老版本都在运行,但是流量还都在老版本上。

第三步:配置灰度规则

接下来是关键步骤,我们要配置灰度规则。这里有两种常用的方式:

方式一:基于请求头的灰度(适合灰度发布以及AB测试场景)

这种方式适合给特定用户开放新功能,比如内部测试人员或者VIP用户。

创建 ingress-canary-rule1.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gray-release-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "foo"
nginx.ingress.kubernetes.io/canary-by-header-value: "bar"
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- path: /
backend:
service:
name: new-nginx # 指向新版本服务
port:
number: 80
pathType: ImplementationSpecific

应用配置:

1
kubectl apply -f ingress-canary-rule1.yaml

测试效果:

1
2
3
4
5
# 普通请求,还是访问老版本
curl -H "Host: www.example.com" http://<EXTERNAL_IP>

# 带上特定请求头,访问新版本
curl -H "Host: www.example.com" -H "foo: bar" http://<EXTERNAL_IP>

这样就实现了精准的流量控制,只有带特定请求头的请求才会路由到新版本。实际使用中,可以在客户端给测试用户的请求加上这个header,或者用 X-Canary-Version 这种更语义化的header名。

除了基于请求头,还可以使用基于cookie的方式,只需将annotation改为:

1
nginx.ingress.kubernetes.io/canary-by-cookie: "canary"

这样当请求中包含名为canary的cookie且值为always时,请求会被路由到Canary版本。

方式二:基于权重的灰度(适合蓝绿发布)

这种方式更常用,按百分比逐步放量。

创建 ingress-canary-rule2.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: gray-release-canary
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "50"
spec:
ingressClassName: nginx
rules:
- host: www.example.com
http:
paths:
- path: /
backend:
service:
name: new-nginx # 指向新版本服务
port:
number: 80
pathType: ImplementationSpecific

应用配置:

1
kubectl apply -f ingress-canary-rule2.yaml

测试效果:

1
2
# 多次执行这个命令
curl -H "Host: www.example.com" http://<EXTERNAL_IP>

你会发现大概一半的请求会路由到新版本,一半还在老版本,这就是50%的流量分配。

实际操作中,可以这样逐步放量:

  1. 先设置 canary-weight: "10" 给新版本10%流量,观察一段时间
  2. 没问题就改成 canary-weight: "30",继续观察
  3. 再改成 canary-weight: "50"
  4. 最后改成 canary-weight: "100",或者直接进入下一步

第四步:完全切换到新版本

经过一段时间的观察,新版本运行稳定,没有出现问题,现在可以完全切换到新版本了。

这里有个小技巧:不是直接删除老版本,而是让老版本的Service指向新版本的Deployment。这样做的好处是,如果后面发现问题,可以快速回滚。

修改 old-nginx-deployment-and-service.yaml 中的Service部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: old-nginx
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
run: new-nginx # 改成指向新版本Deployment
sessionAffinity: None
type: NodePort

应用修改:

1
kubectl apply -f old-nginx-deployment-and-service.yaml

验证一下:

1
2
curl -H "Host: www.example.com" http://<EXTERNAL_IP>
# 现在应该全部路由到新版本了

确认没问题后,就可以清理资源了:

1
2
3
4
5
6
7
8
# 删除Canary Ingress
kubectl delete ingress gray-release-canary

# 删除老版本的Deployment
kubectl delete deploy old-nginx

# 删除新版本的Service(因为现在用的是old-nginx这个Service)
kubectl delete svc new-nginx

最佳实践提示:在生产环境中,建议保留老版本的Deployment一段时间(比如一周),并将副本数设置为0,这样如果新版本出现严重问题,可以快速恢复老版本,而不需要重新构建镜像。

至此,整个灰度发布流程就完成了。

实际使用中的一些注意事项

  1. 监控很重要:在灰度过程中,一定要密切关注监控指标,比如错误率、响应时间、CPU和内存使用情况等。

  2. 逐步放量:不要一上来就给50%流量,建议从5%或10%开始,每次翻倍,比如 5% -> 10% -> 25% -> 50% -> 100%。

  3. 设置观察期:每次调整权重后,至少观察15-30分钟,确保没有问题再继续。

  4. 准备回滚方案:虽然灰度发布已经很安全了,但还是要准备好快速回滚的方案。最简单的方法就是把Canary Ingress删掉,流量就全部回到老版本了。

  5. 日志和告警:配置好日志收集和告警规则,一旦出现异常能第一时间发现。

  6. Canary规则注意事项

    • Canary Ingress必须与普通Ingress具有相同的host和path规则
    • 一个普通Ingress只能对应一个Canary Ingress
    • Canary Ingress的创建时间必须晚于普通Ingress
    • 建议给Canary Ingress添加明确的标签,方便管理

总结

使用Nginx Ingress Controller实现灰度发布和蓝绿发布其实并不复杂,核心就是利用Canary机制:

  • 基于请求头的方式适合AB测试和内部测试
  • 基于权重的方式适合逐步放量的蓝绿发布

通过合理配置Canary规则,我们可以实现平滑的应用升级,最大程度降低新版本上线风险,提升用户体验。在实际生产环境中,建议结合监控、日志和告警系统,形成完整的发布流程,确保应用的高可用性和稳定性。