华为云 CCE 灰度发布实战:Ingress 金丝雀、权重流量切换与一键回滚

举报
yd_270427807 发表于 2026/11/15 09:44:37 2026/11/15
【摘要】 引言做过 CCE 线上发布的人,大多经历过这样的夜晚:新版本全量上线后,错误率曲线突然抬头,群里告警刷屏,回滚时才发现镜像标签被覆盖、旧版本 Deployment 已经被删掉,只能凭印象重新编排。问题不在于新版本本身,而在于发布方式——把一次不确定的变更,一次性押给了全部用户。灰度发布就是为了解决这个问题:先让一小部分真实流量验证新版本,指标正常再逐步放量,异常时一键回滚。本文以华为云 C...

引言

做过 CCE 线上发布的人,大多经历过这样的夜晚:新版本全量上线后,错误率曲线突然抬头,群里告警刷屏,回滚时才发现镜像标签被覆盖、旧版本 Deployment 已经被删掉,只能凭印象重新编排。问题不在于新版本本身,而在于发布方式——把一次不确定的变更,一次性押给了全部用户。

灰度发布就是为了解决这个问题:先让一小部分真实流量验证新版本,指标正常再逐步放量,异常时一键回滚。本文以华为云 CCE 为例,用 Ingress 金丝雀规则实现一套可落地的灰度发布流程:从双版本部署、权重放量、按请求头定向灰度,到回滚与生产清单,全部命令与配置都可以在测试集群里直接复现。

一、为什么先做灰度发布:全量发布的真实代价

全量发布最大的风险不是“新版本有 Bug”,而是“发现 Bug 时已经没有退路”。把一次发布拆成可观测、可控制、可回退的若干步,是云原生运维的基本功。灰度发布解决三个具体问题。

第一,缩小爆炸半径。新版本先承接 10% 的流量,即使出现兼容性问题、依赖升级导致的异常,影响面也被控制在十分之一以内;错误预算不会一次烧光。

第二,用真实流量做验证。测试环境再完备,也模拟不了线上真实的请求分布、数据形态和并发特征。灰度流量就是线上验收:同样的请求、同样的用户,只是比例更小。

第三,保留快速回退能力。灰度期间旧版本始终在线,切换只是一条 Ingress 规则的改动,不需要重新构建镜像、不需要等待 CI 流水线,回滚可以在分钟级完成。

一个常见的误区是把“蓝绿发布”和“灰度发布”混为一谈。蓝绿是两套完整环境整体切换,适合能接受瞬间切换的场景;灰度是按比例、按条件逐步切换,适合需要长时间观察的场景。对多数业务系统而言,先灰度、后全量,是最稳妥的节奏。

二、实验环境与前置准备

开始之前,先确认三件事。第一,CCE 集群已创建并处于运行中,节点有足够资源,至少保证能同时跑起两个版本的 Pod。第二,集群已安装 Ingress 控制器。本文以 Nginx Ingress 为例讲解金丝雀注解;如果使用华为云 ELB Ingress,同样可以做多 Service 权重分流,思路相通,只是注解写法不同。第三,本地已配置好 kubectl,并能正常访问集群。

先验证 kubectl 连通性:

kubectl cluster-info
kubectl get nodes
kubectl get pods -A | head -20

看到节点 Ready、能列出系统命名空间下的 Pod,说明访问正常。接下来创建一个独立的命名空间,把实验资源和业务命名空间隔离开:

kubectl create namespace canary-demo
kubectl config set-context --current --namespace=canary-demo

镜像准备上,建议使用自有镜像仓库中的版本化镜像,例如 swr.cn-north-4.myhuaweicloud.com/demo/webapp:v1.0.0 与 v2.0.0。这里要强调一个原则:生产发布不要使用 latest 标签。latest 会被反复覆盖,回滚时无法确定旧版本到底是哪个镜像,一旦出事,连“回到上一版”这个动作本身都变得不可靠。

三、部署双版本示例应用:v1 稳定版与 v2 金丝雀版

我们部署一个最简单的 Web 应用,两个版本除了镜像标签和返回内容不同,其余配置保持一致。先部署稳定版 v1,副本数 3:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-v1
  labels:
    app: webapp
    version: v1
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
      version: v1
  template:
    metadata:
      labels:
        app: webapp
        version: v1
    spec:
      containers:
      - name: webapp
        image: swr.cn-north-4.myhuaweicloud.com/demo/webapp:v1.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 15
          periodSeconds: 10

再部署金丝雀版 v2,副本数先给 1,配置与 v1 基本相同,只是标签和镜像不同:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp-v2
  labels:
    app: webapp
    version: v2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp
      version: v2
  template:
    metadata:
      labels:
        app: webapp
        version: v2
    spec:
      containers:
      - name: webapp
        image: swr.cn-north-4.myhuaweicloud.com/demo/webapp:v2.0.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            cpu: 500m
            memory: 512Mi
        readinessProbe:
          httpGet:
            path: /healthz
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5

两个版本各建一个 Service。Service 的选择器必须精确区分版本标签,否则流量会串到错误的版本上:

apiVersion: v1
kind: Service
metadata:
  name: webapp-v1
spec:
  selector:
    app: webapp
    version: v1
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: webapp-v2
spec:
  selector:
    app: webapp
    version: v2
  ports:
  - port: 80
    targetPort: 8080

一次性应用并检查状态:

kubectl apply -f webapp-v1.yaml
kubectl apply -f webapp-v2.yaml
kubectl apply -f webapp-svc.yaml
kubectl get pods -l app=webapp
kubectl get svc webapp-v1 webapp-v2

kubectl get pods 应看到 3 个 v1 Pod 与 1 个 v2 Pod 全部 Running,且就绪探针通过。有一个细节值得注意:v2 的副本数先给 1,是因为灰度初期流量小,不需要太多副本;放量时再按比例扩容。反过来,如果 v2 一开始就给 3 副本,也不是错误,只是资源占用略多。

四、接入 Ingress:一条规则同时承载两个版本

Ingress 是灰度流量的总入口。先创建指向稳定版的主 Ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: webapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: webapp-v1
            port:
              number: 80

再创建一条金丝雀 Ingress,指向 v2 Service。关键在于注解:canary: "true" 声明这是一条金丝雀规则,canary-weight: "10" 表示 10% 的流量进入 v2:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: webapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: webapp-v2
            port:
              number: 80

两条规则的 host 与 path 必须完全一致,金丝雀规则才会被正确匹配。应用并确认:

kubectl apply -f ingress.yaml
kubectl apply -f ingress-canary.yaml
kubectl get ingress
kubectl describe ingress webapp-canary

此时访问域名,约一成请求会返回 v2 的内容。可以用一个循环直观地验证权重是否生效:

for i in $(seq 1 100); do
  curl -s http://webapp.example.com/ | grep -o 'version[^<]*'
done | sort | uniq -c

如果输出里 v2 大约出现 10 次、v1 大约出现 90 次,说明权重分流已经生效。注意做这个验证时要绕过本地缓存,并且不要用浏览器反复刷新——浏览器会复用连接,分流结果看起来会不均匀,用 curl 循环统计才准确。

五、按权重放量:10% → 50% → 100% 的标准动作

权重放量不是一次改到 100%,而是分阶段、有观察窗口地推进。一个稳妥的节奏是 10% → 50% → 100%,每一步之间留出足够的观察时间。

第一阶段 10%,观察至少 30 分钟。重点看三个指标:v2 的 5xx 错误率是否明显高于 v1,P99 延迟是否异常,Pod 是否有 OOMKilled 或频繁重启。可以在云监控 CES 里把两个版本的指标放在同一张图上对比,也可以直接查日志:

kubectl logs -l app=webapp,version=v2 --tail=200 | grep -iE 'error|exception|timeout' | head -20
kubectl top pods -l app=webapp

如果指标平稳,把权重调到 50%。改动只有一个数字,编辑金丝雀 Ingress 的注解即可:

kubectl annotate ingress webapp-canary nginx.ingress.kubernetes.io/canary-weight=50 --overwrite

同时把 v2 的副本数扩到与 v1 相当,避免少量 Pod 承接一半流量导致过载:

kubectl scale deployment webapp-v2 --replicas=3
kubectl rollout status deployment/webapp-v2

第二阶段 50%,观察一个业务高峰时段。灰度期间最容易被忽略的是依赖:v2 可能调用了新版本的下游接口、新的缓存键、新的数据库字段,这些问题在低流量时不一定暴露。放量到一半时,数据库连接数、缓存命中率、下游超时率都要看一遍。

确认无异常后,把权重调到 100%,此时全部流量进入 v2。再稳定观察一段时间,就可以拆除金丝雀规则,把主 Ingress 的后端改成 v2 Service,并删除 v1 的 Deployment 与 Service,完成一次完整的灰度发布:

kubectl annotate ingress webapp-canary nginx.ingress.kubernetes.io/canary-weight=100 --overwrite
kubectl delete ingress webapp-canary
kubectl delete deployment webapp-v1
kubectl delete service webapp-v1

六、按请求头灰度:只让测试流量走新版本

权重分流是按比例随机分配,适合放量阶段;但在灰度初期,我们往往更想“只让内部测试账号、只让某个地区的用户”先体验新版本。这时要用请求头或 Cookie 做定向灰度。

Nginx Ingress 支持按请求头匹配:带指定请求头、且值为指定内容的请求进入金丝雀版本:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: webapp-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-by-header: "x-canary"
    nginx.ingress.kubernetes.io/canary-by-header-value: "v2"
spec:
  ingressClassName: nginx
  rules:
  - host: webapp.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: webapp-v2
            port:
              number: 80

验证时带上请求头:

curl -s -H 'x-canary: v2' http://webapp.example.com/ | head -5
curl -s http://webapp.example.com/ | head -5

第一条命令应稳定返回 v2,第二条返回 v1。按请求头灰度也可以和权重组合使用:先让测试人员用请求头验证功能,再用权重放量验证性能与稳定性。

如果业务是浏览器访问,请求头不方便携带,可以改用 Cookie 规则:

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

设置了 canary=v2 这个 Cookie 的浏览器会被路由到新版本,测试同学在浏览器控制台里写一行 document.cookie="canary=v2" 就能进入灰度,清理 Cookie 即可退出,操作成本很低。

七、异常回滚:一键切回稳定版

灰度发布的价值,有一半体现在回滚速度上。灰度期间 v1 始终在线,回滚不需要重新部署,只需要把金丝雀规则删掉:

kubectl delete ingress webapp-canary
kubectl get ingress

删除金丝雀 Ingress 后,所有流量立即回到 v1。也可以只把权重改回 0,保留规则以便事后分析:

kubectl annotate ingress webapp-canary nginx.ingress.kubernetes.io/canary-weight=0 --overwrite

如果问题出在 Deployment 本身,例如 v2 的镜像有缺陷,也可以用 rollout 回滚:

kubectl rollout undo deployment/webapp-v2
kubectl rollout status deployment/webapp-v2
kubectl rollout history deployment/webapp-v2

建议每个月至少做一次回滚演练:随机挑一个非高峰时段,模拟 v2 异常,计时从发现异常到流量完全切回 v1 需要多久。很多团队第一次演练才发现回滚命令记错了命名空间、或者旧版本 Deployment 早已被清理,这些问题平时看不出来,出事时却致命。回滚演练的另一个产出是“回滚清单”:写清楚谁有权执行回滚、执行哪几条命令、回滚后如何通知、如何验证流量已经切回,把清单贴在值班手册里。

八、生产落地清单与常见坑

把灰度发布用到生产之前,先逐项确认下面的清单。

探针必须配好。就绪探针决定 Pod 是否进入 Service 的负载均衡列表,没有就绪探针的 Pod 会在还没准备好时就收到流量;存活探针则负责重启真正僵死的进程。两个探针的路径要真实可用,不要用根路径 / 充数,根路径返回 200 不代表业务就绪。

注意会话亲和性。如果业务依赖会话保持,例如登录态存在本地内存,权重分流会让同一个用户在 v1 与 v2 之间来回跳,表现为“刚登录又掉线”。这种场景要先评估会话存储是否外置到 Redis 等共享存储,或者在灰度期间开启会话亲和。

镜像标签与资源配额。灰度期间两个版本同时运行,节点资源占用会比平时高,放量前先确认命名空间的 ResourceQuota 和节点剩余资源是否够用;镜像务必使用不可变的版本标签,禁止 latest。

缓存与静态资源。新版本的前端静态资源文件名如果带了哈希,灰度期间新旧资源共存一般没问题;如果文件名固定不变,CDN 或浏览器缓存可能让用户拿到新旧混杂的页面。发布前确认缓存策略,或者让前端资源走版本化路径。

数据库变更要先行。灰度发布只解决应用层的切换,数据库字段、索引、兼容性问题需要提前处理。基本原则是“先加字段、再发布、最后删字段”,新版本代码要同时兼容新旧两种数据结构,否则灰度切回旧版本时,旧代码读到新结构的数据会直接报错。

指标与告警先于发布。灰度开始前,确认错误率、延迟、Pod 重启、资源水位这四类告警都已配置,并且值班同学知道灰度正在进行。没有观测能力的灰度,和全量发布没有本质区别。

最后是权限与审计。生产集群的 Ingress 修改权限要收敛到发布负责人,操作记录保留在审计日志里;权重调整这类高危操作,建议通过流水线或审批流程执行,而不是任何人都能随手改注解。

总结

灰度发布的核心思想,是把一次不确定的全量变更,拆成“小步快跑、随时可退”的若干步。本文用华为云 CCE 与 Nginx Ingress 金丝雀规则,完整走了一遍这个流程:双版本部署保证新旧并存,Ingress 金丝雀规则控制流量比例,10% → 50% → 100% 分阶段放量,请求头与 Cookie 定向灰度满足测试需求,删除一条 Ingress 规则即可完成回滚。

这套流程不需要引入额外的发布平台,全部能力都来自 Kubernetes 原生对象与 CCE 的 Ingress 能力,适合大多数中小规模业务直接落地。更进一步,可以把权重调整、指标比对、自动回滚接入 CI/CD 流水线,做成自动化的渐进式发布;也可以结合华为云云监控 CES 与应用运维管理 AOM,把灰度期间的指标对比做成固定报表。

发布策略的终极目标,是让发布这件事变得无聊:不再需要熬夜守着上线,不再需要“赌一把”的勇气。先从下一次发布开始,把 10% 的流量交给金丝雀,剩下的 90%,交给时间与指标来决定。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。