运维知识
悠悠
2026年9月3日

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 匹配上了就不管权重了。

踩坑点

  1. 一个域名只能有一个 canary Ingress。想灰度多个版本?不行,只能两个版本之间切。
  2. canary-weight 的值是字符串,写成 canary-weight: 10(不带引号)在某些版本下会报错。
  3. Ingress 的 host 必须一致,稳定版和金丝雀版的 Ingress 域名不一样的话根本不会走 canary 逻辑。
  4. 会话粘性问题:同一个用户可能一会儿访问旧版一会儿访问新版。如果业务对这个敏感,需要配合 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)

踩坑点

  1. 两个 weight 之和必须等于 100。写成 90 和 5 会报验证错误——别笑,真有人这么干过。
  2. VirtualService 的 match 规则顺序很重要,Istio 是从上往下匹配的,第一个匹配到的规则就会生效。把兜底路由放在最前面会导致所有请求都走默认路由。
  3. Sidecar 注入问题:如果某些 Pod 没有被注入 Envoy Sidecar,那 VirtualService 的流量规则对它不生效。用 kubectl get pod -o jsonpath='{.spec.containers[*].name}' 确认一下容器列表里有没有 istio-proxy
  4. 资源开销: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 里多了 canaryblueGreen 两种策略。

基础灰度配置

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

踩坑点

  1. Rollout 和 Deployment 不能共存。如果你是从 Deployment 迁移过来,要先删掉 Deployment 再创建 Rollout,不然两个控制器会打架。官方推荐的迁移方式是用 kubectl apply 时加上 --force 参数,但更稳妥的做法是先 scale down Deployment 到 0 再创建 Rollout。
  2. AnalysisTemplate 的 Prometheus 查询语句一定要先在 Grafana 里验证。查询写错了不会报语法错误,而是返回空结果,控制器可能把空结果当成"通过"处理。
  3. trafficRouting 的配置取决于你用的 Ingress Controller。用 Nginx Ingress 的配 nginx,用 Istio 的配 istio,用 ALB 的配 alb,别搞混了。
  4. pause 没有 duration 就是无限期暂停,需要手动 kubectl argo rollouts promote myapp 才能继续。这不是 Bug,是故意设计的——方便你在某些关键节点做人工确认。

三种方案的对比

维度Nginx Ingress CanaryIstioArgo 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周观察

说到底,灰度发布不是什么高深技术,核心就是"控制变更的爆炸半径"。选哪个方案不重要,重要的是你真的在用灰度发布,而不是每次都直接全量上线然后祈祷。


我是「运维躬行录」,专注分享云计算和运维的生产实践经验。如果这篇文章对你有帮助,帮忙点个赞、转发一下。 关注公众号:耕云躬行录 个人博客:躬行笔记

文章目录

博主介绍

热爱技术的云计算运维工程师,Python全栈工程师,分享开发经验与生活感悟。
欢迎关注我的微信公众号@运维躬行录,领取海量学习资料

微信二维码