3套开源灰度发布方案,从Nginx Ingress到Argo Rollouts我全踩了一遍
上周五晚上11点,刚上线的支付接口把10%的用户请求打到了一个有Bug的新版本上,告警群炸了。回滚倒是快,但老板第二天问了一句:"咱们灰度方案到底靠不靠谱?"
说实话,这事让我重新审视了一遍手头的灰度发布体系。趁周末把 Nginx Ingress Canary、Istio 流量管理、Argo Rollouts 这三套方案从头到尾跑了一遍,记录下来,给还在纠结选型的兄弟们一个参考。
目前 K8s 生态里能拿来做灰度的开源方案,我觉得值得认真看的就这三个:
- Nginx Ingress Canary Annotations:最轻量,改几个 annotation 就行
- Istio VirtualService + DestinationRule:功能最强,但引入了服务网格的复杂度
- Argo Rollouts:专门干这事的 CRD 控制器,支持自动化渐进式交付
一个个来。
方案一:Nginx Ingress Canary——改几行注解搞定灰度
如果你的集群已经在用 ingress-nginx(注意是 kubernetes/ingress-nginx 而不是 nginxinc/nginx-ingress,别搞混),那这个方案几乎零成本接入。
原理
ingress-nginx 从 0.21 版本开始支持 Canary 注解。本质上就是建两个 Ingress 资源指向同一个域名,一个指向稳定版 Service,另一个带上 nginx.ingress.kubernetes.io/canary: "true" 注解指向金丝雀版 Service。Nginx 根据注解规则决定把请求转发到哪个后端。
实操步骤
先部署一个稳定版应用,假设是个简单的 Web 服务:
# stable-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-stable
labels:
app: myapp
version: stable
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: stable
template:
metadata:
labels:
app: myapp
version: stable
spec:
containers:
- name: myapp
image: myregistry/myapp:v1.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: myapp-stable
spec:
selector:
app: myapp
version: stable
ports:
- port: 80
targetPort: 8080
再部署金丝雀版本(新版本),副本数可以少一些:
# canary-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-canary
labels:
app: myapp
version: canary
spec:
replicas: 1
selector:
matchLabels:
app: myapp
version: canary
template:
metadata:
labels:
app: myapp
version: canary
spec:
containers:
- name: myapp
image: myregistry/myapp:v2.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: myapp-canary
spec:
selector:
app: myapp
version: canary
ports:
- port: 80
targetPort: 8080
然后是关键部分——创建两个 Ingress:
# ingress-stable.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-stable
port:
number: 80
# ingress-canary.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: myapp-ingress-canary
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: myapp-canary
port:
number: 80
canary-weight: "10" 表示 10% 的流量走金丝雀版本。验证一下:
# 连续请求100次,统计返回结果
for i in $(seq 1 100); do
curl -s -H "Host: myapp.example.com" http://<INGRESS_IP>/ | grep -o 'v[0-9]\.[0-9]'
done | sort | uniq -c
正常的话大概能看到 90 次左右返回 v1.0,10 次左右返回 v2.0。
更精细的控制
除了按权重分流,ingress-nginx 还支持按 Header 和 Cookie 分流:
# 按Header分流——测试人员带上特定Header就走金丝雀
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-header: "X-Canary"
nginx.ingress.kubernetes.io/canary-by-header-value: "always"
# 带Header的请求一定走新版本
curl -H "Host: myapp.example.com" -H "X-Canary: always" http://<INGRESS_IP>/
# 不带Header走稳定版
curl -H "Host: myapp.example.com" http://<INGRESS_IP>/
# 按Cookie分流——内部灰度用户
annotations:
nginx.ingress.kubernetes.io/canary: "true"
nginx.ingress.kubernetes.io/canary-by-cookie: "canary_user"
当 cookie canary_user 值为 always 时走金丝雀,值为 never 时走稳定版。
注意优先级:canary-by-header > canary-by-cookie > canary-weight。也就是说设了 Header 规则的情况下,Header 匹配上了就不管权重了。
踩坑点
- 一个域名只能有一个 canary Ingress。想灰度多个版本?不行,只能两个版本之间切。
- canary-weight 的值是字符串,写成
canary-weight: 10(不带引号)在某些版本下会报错。 - Ingress 的 host 必须一致,稳定版和金丝雀版的 Ingress 域名不一样的话根本不会走 canary 逻辑。
- 会话粘性问题:同一个用户可能一会儿访问旧版一会儿访问新版。如果业务对这个敏感,需要配合
nginx.ingress.kubernetes.io/affinity: cookie来保持会话一致性。
方案二:Istio——服务网格级别的精细流量管理
如果你的集群已经上了 Istio,用 VirtualService + DestinationRule 做灰度会优雅很多,功能也强大得多。
准备工作
假设 Istio 已经装好了,命名空间开启了自动注入:
kubectl label namespace default istio-injection=enabled
部署两个版本的应用。注意这里用同一个 Service,通过 Pod label 来区分版本:
# deployment-v1.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v1
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: v1
template:
metadata:
labels:
app: myapp
version: v1
spec:
containers:
- name: myapp
image: myregistry/myapp:v1.0
ports:
- containerPort: 8080
---
# deployment-v2.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-v2
spec:
replicas: 1
selector:
matchLabels:
app: myapp
version: v2
template:
metadata:
labels:
app: myapp
version: v2
spec:
containers:
- name: myapp
image: myregistry/myapp:v2.0
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp # 注意这里只匹配app标签,两个版本的Pod都会被选中
ports:
- port: 80
targetPort: 8080
用 DestinationRule 定义子集
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp-dr
spec:
host: myapp
subsets:
- name: v1
labels:
version: v1
- name: v2
labels:
version: v2
用 VirtualService 分配流量权重
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-vs
spec:
hosts:
- myapp
http:
- route:
- destination:
host: myapp
subset: v1
weight: 90
- destination:
host: myapp
subset: v2
weight: 10
这样集群内部调用 myapp 这个 Service 时,90% 的流量走 v1,10% 走 v2。
按请求头做精准灰度
比测试人员要验证新版本,可以通过 Header 路由:
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp-vs
spec:
hosts:
- myapp
http:
- match:
- headers:
x-user-group:
exact: beta
route:
- destination:
host: myapp
subset: v2
- route:
- destination:
host: myapp
subset: v1
带上 x-user-group: beta 头的请求全走 v2,其余全走 v1。这个在做内部灰度测试的时候非常好使。
逐步放量的操作流程
实际生产中灰度发布通常是个渐进过程,手动改 VirtualService 的权重就行:
# 第一步:5%流量
kubectl patch virtualservice myapp-vs --type merge -p '
spec:
http:
- route:
- destination:
host: myapp
subset: v1
weight: 95
- destination:
host: myapp
subset: v2
weight: 5'
# 观察10分钟,没问题继续
# 第二步:20%流量
kubectl patch virtualservice myapp-vs --type merge -p '
spec:
http:
- route:
- destination:
host: myapp
subset: v1
weight: 80
- destination:
host: myapp
subset: v2
weight: 20'
# 继续观察,没问题放到50%,最后100%
# 全量切换
kubectl patch virtualservice myapp-vs --type merge -p '
spec:
http:
- route:
- destination:
host: myapp
subset: v2
weight: 100'
验证流量分布可以看 Kiali 的流量图,或者直接用 Prometheus 查询:
# 查看各版本的请求速率
sum(rate(istio_requests_total{destination_service="myapp.default.svc.cluster.local"}[1m])) by (destination_version)
踩坑点
- 两个 weight 之和必须等于 100。写成 90 和 5 会报验证错误——别笑,真有人这么干过。
- VirtualService 的 match 规则顺序很重要,Istio 是从上往下匹配的,第一个匹配到的规则就会生效。把兜底路由放在最前面会导致所有请求都走默认路由。
- Sidecar 注入问题:如果某些 Pod 没有被注入 Envoy Sidecar,那 VirtualService 的流量规则对它不生效。用
kubectl get pod -o jsonpath='{.spec.containers[*].name}'确认一下容器列表里有没有istio-proxy。 - 资源开销:Istio 的 istiod 控制面本身需要一定资源,每个 Pod 多挂一个 Sidecar 也有 CPU 和内存开销。小集群上了 Istio 就为了做灰度,有点杀鸡用牛刀。
方案三:Argo Rollouts——专业的渐进式交付控制器
前面两个方案本质上都是"手动灰度"——流量比例需要人去改,回滚也要人操作。Argo Rollouts 解决的就是自动化的问题:它能根据 Prometheus 指标自动判断新版本是否健康,健康就逐步加大流量,不健康就自动回滚。
安装
# 安装 Argo Rollouts 控制器
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# 安装 kubectl 插件(可选但推荐)
# Linux
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
# 验证
kubectl argo rollouts version
核心概念
Argo Rollouts 引入了一个叫 Rollout 的 CRD,它是 Deployment 的超集——语法几乎一样,但 spec.strategy 里多了 canary 和 blueGreen 两种策略。
基础灰度配置
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 5
revisionHistoryLimit: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:v1.0
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
strategy:
canary:
canaryService: myapp-canary
stableService: myapp-stable
trafficRouting:
nginx:
stableIngress: myapp-ingress # 已有的稳定版Ingress名称
steps:
- setWeight: 5
- pause: {duration: 2m} # 5%流量跑2分钟
- setWeight: 20
- pause: {duration: 5m} # 20%流量跑5分钟
- setWeight: 50
- pause: {duration: 10m} # 50%流量跑10分钟
- setWeight: 80
- pause: {duration: 5m} # 80%流量跑5分钟
# 最后自动全量
配套的 Service:
apiVersion: v1
kind: Service
metadata:
name: myapp-stable
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: myapp-canary
spec:
selector:
app: myapp
ports:
- port: 80
targetPort: 8080
触发灰度只需要改镜像版本:
kubectl argo rollouts set image myapp myapp=myregistry/myapp:v2.0
# 实时观察发布状态
kubectl argo rollouts get rollout myapp --watch
输出会实时展示当前走到哪一步、流量比例多少、各 ReplicaSet 的 Pod 数量,非常直观。
结合 Prometheus 指标实现自动化判断
这才是 Argo Rollouts 的杀手锏——定义 AnalysisTemplate,让控制器自动判断金丝雀版本是否正常:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 60s
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.."}[2m]))
/
sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))
然后在 Rollout 的 steps 里引用:
strategy:
canary:
steps:
- setWeight: 5
- pause: {duration: 2m}
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: myapp-canary
- setWeight: 20
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 10m}
这段配置的意思是:流量放到 5% 跑 2 分钟后,开始分析金丝雀版本的成功率。每 60 秒查一次 Prometheus,如果成功率低于 95% 累计超过 3 次,自动回滚。一切正常的话继续放量到 20%、50%。
手动回滚也很简单:
# 发现问题,立即中止并回滚
kubectl argo rollouts abort myapp
# 如果需要重新发布
kubectl argo rollouts retry rollout myapp
踩坑点
- Rollout 和 Deployment 不能共存。如果你是从 Deployment 迁移过来,要先删掉 Deployment 再创建 Rollout,不然两个控制器会打架。官方推荐的迁移方式是用
kubectl apply时加上--force参数,但更稳妥的做法是先 scale down Deployment 到 0 再创建 Rollout。 - AnalysisTemplate 的 Prometheus 查询语句一定要先在 Grafana 里验证。查询写错了不会报语法错误,而是返回空结果,控制器可能把空结果当成"通过"处理。
- trafficRouting 的配置取决于你用的 Ingress Controller。用 Nginx Ingress 的配
nginx,用 Istio 的配istio,用 ALB 的配alb,别搞混了。 - pause 没有 duration 就是无限期暂停,需要手动
kubectl argo rollouts promote myapp才能继续。这不是 Bug,是故意设计的——方便你在某些关键节点做人工确认。
三种方案的对比
| 维度 | Nginx Ingress Canary | Istio | Argo Rollouts |
|---|---|---|---|
| 复杂度 | 低,改注解即可 | 高,需要服务网格 | 中,装个控制器 |
| 流量粒度 | 权重/Header/Cookie | 权重/Header/Cookie/URI等 | 取决于底层路由方案 |
| 自动化 | 无,纯手动 | 无,纯手动 | 支持,基于指标自动升降级 |
| 回滚 | 手动删canary Ingress | 手动改VirtualService | 自动或手动 |
| 多版本灰度 | 不支持(只能1个canary) | 支持(多个subset) | 支持(多个revision) |
| 额外依赖 | 无(已有ingress-nginx即可) | Istio全家桶 | Argo Rollouts控制器 |
| 适合场景 | 小团队快速灰度 | 已有Istio的微服务架构 | 需要自动化渐进式交付 |
我的选型建议
根据我在几个项目上的实践经验:
10人以下的团队,服务不超过20个:直接用 Nginx Ingress Canary。不需要引入额外组件,学习成本低,在 CI/CD 流水线里加几行 kubectl patch 就能实现自动灰度。
已经上了 Istio 的微服务架构:用 VirtualService 做灰度是顺理成章的事。Istio 的流量管理能力远不止灰度,故障注入、熔断、限流都能一起搞定。
对发布自动化要求高、有专职平台团队维护:上 Argo Rollouts。它和 Argo CD 配合得很好,能实现 GitOps + 渐进式交付的完整流程。而且 Argo Rollouts 可以对接 Nginx Ingress、Istio、ALB 等多种流量管理方案,灵活性很高。
还有一个方案值得一提——Flagger。它和 Argo Rollouts 定位类似,都是渐进式交付控制器,区别在于 Flagger 是 Flux 生态的(由 Weaveworks 开发),对 Flux CD 的集成更好。如果你用 Flux 做 GitOps,Flagger 是更自然的选择。
时间线总结
| 阶段 | 操作 | 预计耗时 |
|---|---|---|
| 方案评估 | 根据团队规模和技术栈选择方案 | 1天 |
| 环境准备 | 安装/升级对应组件 | Nginx Ingress: 0 / Istio: 半天 / Argo Rollouts: 1小时 |
| 配置编写 | 编写灰度相关YAML | 半天 |
| 测试验证 | 在测试环境跑通完整流程 | 1天 |
| 接入CI/CD | 在流水线中集成灰度步骤 | 半天 |
| 生产验证 | 选一个低风险服务先跑起来 | 1-2周观察 |
说到底,灰度发布不是什么高深技术,核心就是"控制变更的爆炸半径"。选哪个方案不重要,重要的是你真的在用灰度发布,而不是每次都直接全量上线然后祈祷。
我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。 关注公众号:耕云躬行录 个人博客:躬行笔记