跳到正文

灰度发布与交付平台 ​

一句话:发布策略的本质是控制爆炸半径——先让一小部分流量承担新版本的风险,用可观测指标决定继续放量还是回滚;换来的是安全,代价是额外的路由、容量和观测投入。

适用版本:Kubernetes 1.29+,ingress-nginx 1.9+(canary 注解),Istio 1.20+。注解与默认值以官方文档为准。

为什么需要 ​

滚动更新会在几分钟内把所有 Pod 换成新版本:第一批出问题时,后面还在继续换,等发现错误率上升,大部分流量已经打到坏版本上。蓝绿能瞬间切回,但两套环境同时跑,容量翻倍,数据库变更仍然绕不开。

灰度发布把「全量」拆成「先 10%,观察,再 30%、50%、100%」。它的价值不在发布速度,而在出问题时只影响一小部分用户,并且有明确的指标和回滚动作。

核心机制 ​

三种策略的取舍 ​

策略流量形态资源代价回滚速度适用
滚动更新新旧 Pod 并存,逐步替换基本不增加再滚动一次,较慢无状态、变更风险低
蓝绿两套完整环境,入口一次切换接近翻倍切回入口,秒级需要瞬间回滚、能承担双倍容量
灰度新旧版本按比例或特征分流增加一个灰度实例组权重归零,秒级核心服务、变更风险高

蓝绿解决「切得快」,灰度解决「影响小」,滚动解决「资源省」。三者不是互斥的:灰度环境内部通常还是用滚动更新把灰度实例组从 1 个扩到 N 个。

流量切分发生在哪一层 ​

  • L4 Service:只做四层转发,不能按比例分流量。要做灰度得建两个 Deployment、两个 Service,再由上层按权重转发。
  • L7 Ingress:ingress-nginx 用注解实现 canary-weight(按请求比例)、canary-by-header、canary-by-cookie。权重是请求级的,同一个用户的连续请求可能落到不同版本。
  • Service Mesh:VirtualService 同时支持权重和 header/URI 匹配,能把「内部员工」「特定租户」固定到灰度版本,粒度比 Ingress 细。
  • API 网关:在入口做权重和断言组合,适合多语言服务;但网关之后的调用链不受控,跨服务灰度仍要 Mesh 或 RPC 路由。

越往下(L7、Mesh)控制越细,代价是组件更多、链路更长。只做一次简单灰度,Ingress 注解就够了;要做全链路灰度,才需要 Mesh 和标签路由。

观测与回滚 ​

灰度期间要回答两个问题:新版本是否比旧版本差,差多少。所以指标必须对比看,不能只看绝对值:

指标对比方式判断
5xx 比例灰度组 vs 基线组灰度显著高于基线才回滚
P99 延迟同一接口、同一时间段排除流量结构差异
资源饱和度CPU throttling、内存、连接池新版本是否更吃资源
业务指标下单成功率、支付转化技术指标正常但业务异常也要回滚

观察窗口可配 10–300s,默认按平台配置;批处理或低 QPS 服务取长窗口,API 服务取短窗口。窗口太短样本不够,指标噪声会把正常发布误判为故障。

自动回滚的触发条件通常有三类:5xx 比例或 P99 超过阈值、readiness 连续失败、人工确认超时(超时时间由平台配置)。回滚动作要区分场景:

bash
# 同一 Deployment 的滚动发布:回到上一个 revision
kubectl rollout undo deployment/order --to-revision=3
kubectl rollout status deployment/order

# 独立灰度组:先把权重归零,确认流量回落后再删
kubectl annotate ingress order-canary \
  nginx.ingress.kubernetes.io/canary-weight=0 --overwrite
kubectl delete deployment order-canary
kubectl delete ingress order-canary

数据库变更不能靠 rollout undo 回滚。发布前要把 schema 变更拆成「先加兼容字段 → 双写/回填 → 切读 → 再删旧字段」,否则旧版本代码读到新结构会直接报错。

发布前的三个一致性检查 ​

  1. Service selector 与 Pod label 一致:灰度时最容易在这里出 502——新 Pod 的 label 改了,Service 还选旧 label,Endpoint 为空。
  2. 配置与镜像版本匹配:ConfigMap 里的开关、注册中心的路由标签要和灰度版本一起发布,否则新代码读到旧配置。
  3. 回滚路径存在:kubectl rollout history 有上一版,镜像 tag 可追溯,数据库变更可兼容。

最小上手 ​

用两个 Deployment 加一个 canary Ingress 演示按比例切流:

bash
kubectl create deployment order-v1 --image=nginx:1.27 --replicas=3
kubectl create deployment order-v2 --image=nginx:1.27 --replicas=1
kubectl expose deployment order-v1 --name=order-v1 --port=80 --target-port=80
kubectl expose deployment order-v2 --name=order-v2 --port=80 --target-port=80
yaml
# order-canary.yaml,主 Ingress 指向 order-v1,这个只挂灰度
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-canary
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
    - host: order.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-v2
                port: {number: 80}
bash
kubectl apply -f order-canary.yaml
kubectl annotate ingress order-canary \
  nginx.ingress.kubernetes.io/canary-weight=30 --overwrite
# 观察指标后继续 50 → 100;异常则把 weight 设为 0 并删除灰度资源

前提:同一 host 已有一条指向 order-v1 的主 Ingress,canary 注解按权重叠加在它上面;只有灰度 Ingress 时,流量会全部落到灰度后端。

生产动作 ​

  • 先定回滚条件再发布:错误率、P99、readiness 的阈值和观察窗口写进平台,不靠人盯面板。
  • 灰度组容量要够:灰度实例太少,指标没有统计意义;至少覆盖一个完整的故障域。
  • 保持标签可区分:基线和灰度用不同 label,但 Service selector 只选基线;灰度 Ingress 单独指向灰度 Service。
  • 优雅停机:preStop 先从注册中心/SLB 摘除,再等 terminationGracePeriodSeconds(默认 30s,以官方文档为准)让存量请求处理完;否则滚动或回滚时会出现 502。
  • 发布记录:镜像用 digest 或固定 tag,revisionHistoryLimit(默认 10,以官方文档为准)保留足够历史;记录每次权重变更和指标快照。

边界 ​

  • 权重是请求级,不是用户级:同一用户可能一会新一会旧;有会话状态的服务要用 cookie/header 粘住。
  • 灰度抓不到数据问题:schema 不兼容、历史数据缺失,只有全量或回放才暴露;数据库变更必须前后兼容。
  • 低流量服务不适合小比例灰度:10% 流量可能只有几十个请求,指标噪声大于真实差异。
  • Mesh 不是免费的:Sidecar 增加一跳延迟和运维复杂度,小规模团队用 Ingress 注解更划算。
  • 自动回滚不是万能的:它只能回滚工作负载,回滚不了已经写入的数据和已经发出的消息。

讲清楚 ​

发布策略的核心是爆炸半径。滚动更新资源省,但新版本会在几分钟内铺满;蓝绿切得快,但容量翻倍;灰度让一小部分流量先试,用指标决定继续还是回滚。

流量切分的位置决定控制粒度:Service 是四层,分不了比例;Ingress 能按权重和 header 分流;Mesh 能按请求特征做更细的路由,代价是多一层组件。

灰度真正难的不是切 10%,而是观测和回滚:指标要跟基线对比,窗口要够长,回滚要区分工作负载和数据库。数据库变更必须前后兼容,否则代码回滚了,数据回不去。

把一门技术讲透到既懂也会用