主题
灰度发布与交付平台
一句话:发布策略的本质是控制爆炸半径——先让一小部分流量承担新版本的风险,用可观测指标决定继续放量还是回滚;换来的是安全,代价是额外的路由、容量和观测投入。
适用版本: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 变更拆成「先加兼容字段 → 双写/回填 → 切读 → 再删旧字段」,否则旧版本代码读到新结构会直接报错。
发布前的三个一致性检查
- Service selector 与 Pod label 一致:灰度时最容易在这里出 502——新 Pod 的 label 改了,Service 还选旧 label,Endpoint 为空。
- 配置与镜像版本匹配:ConfigMap 里的开关、注册中心的路由标签要和灰度版本一起发布,否则新代码读到旧配置。
- 回滚路径存在:
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=80yaml
# 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%,而是观测和回滚:指标要跟基线对比,窗口要够长,回滚要区分工作负载和数据库。数据库变更必须前后兼容,否则代码回滚了,数据回不去。