2827 字
约 9 分钟
16
S16-X13 · 灰度发布与回滚验证
2024-12-31
2026-11-08

一个"只改了一行缓存 key"的版本,发布后十分钟订单量掉了三成——问题不是那行代码,而是新 key 让缓存全部未命中,数据库连接池在三十秒内被击穿。代码本身可回滚,五分钟就退了;但已经写进库里的新格式数据回不去,恢复花了四个小时。

灰度要处理的就是这类事:把"一次全量发布"切成"多次小规模暴露",再配一条随时能走通、且真的走过一次的回头路。


1. 灰度解决什么,不解决什么

灰度能解决 灰度解决不了
新代码的缺陷影响面可控 数据迁移已经执行、无法回退
性能回归在小流量下被发现 双写/协议不兼容导致的互斥
可量化地比较新旧版本 配置中心的全局开关误改
提供"停止放量"这一动作 依赖方的配额、证书、DNS 变更

**灰度的前提是新旧版本能同时活着。**如果这次变更要求"库表结构只能有一套",灰度就不是可选项,而是必须先做兼容改造。判断顺序是:先问"旧版本能不能和新的共存",再问"按什么维度切"。


2. 分层方式与实现

维度 实现 优点 风险
实例比例 nginx upstream 权重、K8s 副本数 简单、无需改代码 同一用户命中不同版本
流量比例 服务网格(Istio VirtualService)、网关权重 精细可控 依赖网格覆盖度
用户/租户 应用层开关、请求头路由 体验一致、可指定内测用户 需实现分桶逻辑
地域/机房 路由规则、多集群发布 影响面清晰 跨区数据一致性
# nginx 权重灰度:10% 流量走新版本
upstream order_api {
    server 10.0.0.11:8080 weight=9;   # 旧版本
    server 10.0.0.12:8080 weight=1;   # 新版本
    keepalive 64;
}
# Istio:按权重切流
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: {name: order}
spec:
  hosts: [order]
  http:
    - route:
        - destination: {host: order, subset: v1}
          weight: 90
        - destination: {host: order, subset: v2}
          weight: 10

实例比例 vs 流量比例的差别在会话:按实例切,同一用户会来回命中两个版本,如果新版本改了返回结构或本地缓存格式,表现就是"时好时坏"。要求请求级体验一致时,必须按用户分桶。


3. 蓝绿与金丝雀

维度 蓝绿 金丝雀
切换粒度 一次性全量切换 按比例渐进
资源成本 两套完整环境 少量新实例
回滚速度 切回路由,秒级 停止放量并回收少量实例
风险暴露 切换瞬间 100% 线性增长
适用 版本差异大、需整体切换 长尾风险、需要观察期

选择标准很直接:**如果回滚只能靠"切回整环境",蓝绿更稳;如果能接受同一时刻两个版本共存,金丝雀更省。**两者不冲突,常见形态是"新版本先金丝雀放量到 1%,稳定后转蓝绿整体切换"。


4. 观察窗口看什么

观察项 指标形态 与谁比
错误率 5xx / 业务失败率 同期旧版本基线
延迟分位 p95/p99 旧版本同分位
业务指标 下单成功率、支付转化 发布前 24 小时同时段
日志异常量 异常类名计数、WARN 速率 旧版本实例
资源 连接池使用、队列深度 旧版本实例
# 新旧版本错误率对比(按 version 标签分组)
sum by (version) (rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/ sum by (version) (rate(http_server_requests_seconds_count[5m]))

基线必须是同一时刻的旧版本,不是"昨天的全局平均"。整体流量有日周期,拿昨天的数当基线,等于把一个趋势变化误判成版本问题。同时注意样本量:1% 流量下五分钟可能只有几十个请求,错误率的置信区间很宽——放量太慢其实也测不出问题。


5. 回滚的三种形态

形态 动作 耗时量级 风险
切流量 权重归零、路由改回 秒级 新版本写入的数据被旧版本读到
回滚镜像 重新部署旧 tag 分钟级 镜像与配置、依赖版本不匹配
回滚数据 反向脚本、从备份恢复 小时级 回滚期间写入丢失、脚本本身有 bug
# 回滚前先确认:当前部署的版本与实际流量版本
$ kubectl -n prod get deploy order -o jsonpath='{.spec.template.spec.containers[0].image}'
$ kubectl -n prod rollout undo deploy/order --to-revision=REV
$ kubectl -n prod rollout status deploy/order --timeout=180s

顺序永远是"先止血再查因":切流量不需要理解故障,镜像回滚需要版本号准确,数据回滚需要判断依据。事故现场最贵的成本是决策时间,所以把前两种做成一条命令或一个按钮。


6. 回滚前必须验证的四件事

验证项 具体问题 验证方式
数据结构兼容 旧版本读新版本写的行会不会崩 用旧版本连一份含新数据的库跑冒烟
配置向后兼容 旧版本能否解析新配置项 配置项用"未知忽略"策略,实测一次
缓存格式 缓存里的新格式旧版本能否处理 版本前缀化 key,或发布时切 namespace
消息格式 新旧消费者能否共存 schema 兼容规则 + 双消费测试
# 缓存 key 加版本前缀:新版本读 acct:v2:*, 旧版本继续读 acct:v1:*
cache:
  key-prefix: "acct:v2:"
  ttl: 30m

第三条最容易漏:缓存不是数据库,没人给它做兼容评审。旧版本读到新格式的 JSON 反序列化失败时,表现是"缓存穿透 → DB 被打满",看起来像数据库问题。


7. 数据库变更的兼容策略

阶段 动作 是否可回滚
1 加可空列、加索引(前后兼容) 是
2 双写新旧字段 是
3 读切到新字段、回填历史数据 是(回切读旧字段)
4 停止双写 是(恢复双写)
5 删除旧列 否,需备份
-- 阶段 1:只加可空列,旧版本完全无感
ALTER TABLE orders ADD COLUMN channel_code VARCHAR(16) NULL;
-- 阶段 5:删列之前必须先确认没有旧版本实例在跑
SELECT COUNT(*) FROM information_schema.processlist WHERE info LIKE '%channel_code%';

核心原则是**"扩展 → 迁移 → 收缩"三段式**:任何一次发布只跨一段,代码与结构永远有一层冗余。把三步压进一次发布的直接后果是——灰度期间新旧版本必须同时理解两种结构,而这通常没人测。


8. 自动化门禁

# 放量前的自动对比:任一条件不满足就停止放量
canary-gate:
  interval: 60s
  abort_on:
    - metric: error_rate
      condition: "> baseline * 2 + 0.005"
    - metric: p99_latency
      condition: "> baseline * 1.5"
    - metric: business_order_success
      condition: "< baseline * 0.98"
  steps: [1, 5, 25, 50, 100]
要点 说明
相对阈值而非绝对值 不同接口基线差异大
含业务指标 只看技术指标抓不住"成功但算错"
每步有最小观察时长 太短等于没观察
失败即自动停 停比退更快,先停再判断
门禁本身要有测试 门禁失效和没有门禁一样

门禁的价值不是取代人,而是把"要不要继续放量"从一次会议变成一个不需要讨论的判断。


9. 排错:八种灰度事故

现象 原因 处理
灰度实例拖垮数据库 与正式实例共享连接池,新版本连接数翻倍 独立连接池上限,或按比例压低
功能正常但慢 只测功能不测性能 观察窗口强制看 p99
回滚脚本从未跑通 只在文档里存在 每次发布前演练一次回滚
灰度期间配置漂移 有人手动改了 configmap 配置纳入版本控制,发布时比对 diff
新旧版本互相打回 缓存或状态格式不兼容 版本前缀化、schema 兼容
放量后指标"看起来正常" 采样或聚合掩盖问题 同时看实例级与业务指标
回滚后仍错误 数据已按新格式写入 数据回滚方案必须先写
门禁没拦住 基线取自全量而非旧版本 修正对比口径

第一条在 Spring Boot 项目里极常见:两个版本共用同一个 HikariCP 上限配置,但灰度实例数量少、单实例流量集中,看起来"没问题",直到池被打满。容量评估要按"每实例"算,不是按"整体"算。


10. 边界:灰度替代不了的事

场景 为什么不适用 替代
一次性数据订正 数据不能"回滚一部分" 备份 + 幂等脚本 + 对账
不可逆协议变更 新旧无法共存 双协议过渡期
强一致性的分布式变更 半量会出现中间态 停机窗口或两阶段方案
第三方配额/DNS/证书 不在你的流量控制内 变更前确认、预留回退窗口

一句话:**灰度控制的是"暴露量",控制不了"不可逆性"。**凡是不可逆的变更,需要的是备份与演练,不是流量比例。


11. 自测

  1. 按实例比例灰度与按用户分桶,在会话一致性上有什么差别?
  2. 为什么观察基线必须是"同一时刻的旧版本",而不是昨天的全局数据?
  3. 三种回滚形态里哪一种最快,为什么事故现场应该优先用它?
  4. 缓存格式变更若不加版本前缀,回滚后会看到什么现象?
  5. 数据库"扩展 → 迁移 → 收缩"中,为什么不能让一次发布跨两段?
  6. 灰度期间为什么容量要按每实例估算?共享连接池会带来什么后果?
S16-X13 · 灰度发布与回滚验证
https://www.clxhxhhr.top/posts/5836/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。