一次全量上线引发的事故
三个月前我们直接全量发了订单服务 v2,结果一个新分支的序列化逻辑和旧版不兼容,老客户端解不出字段,半小时 rollback 了,期间丢了几十笔订单的回调。复盘会上被喷得不轻。痛定思痛,我搭了一套灰度发布体系,核心四件事:流量染色、网关路由、数据兼容、回滚机制。这套跑顺之后,再没出现过全量故障,最近一次 v3 发布只在 1% 流量上观察到告警就自动止步了。
流量染色
灰度的前提是能区分"新流量"和"老流量"。我们在网关入口给请求打标,标通过请求头透传,下游服务也能读到,用于日志和埋点:
// 网关 Filter:命中灰度规则则加标
if (ruleMatcher.match(exchange)) {
exchange.getRequest().mutate()
.header("X-Canary", "v2")
.build();
}
染色维度可以是 userId 尾号、城市、或者内部白名单。我们先用 1% 的 userId 取模进灰度,验证无误再放量。注意染色要在网关最外层做,避免有些内部调用绕过了标,导致灰度流量统计失真。我们还把染色信息写进日志和 trace,方便事后按版本聚合分析。
网关路由
服务注册时带上版本标签,网关按标路由到对应实例组:
route:
- id: order-v2
predicates:
- Header=X-Canary, v2
uri: lb://order-service-v2
- id: order-stable
uri: lb://order-service
K8s 里 v2 和稳定版是两个 Deployment,共享同一个 Service 名但带不同 version label,这样扩缩容互不干扰,灰度实例挂了也不影响稳定版。我们还给 v2 单独配了 HPA,避免它占用稳定版的资源配额。灰度实例的副本数我们控制在稳定版的 10%,成本控制得住。
数据兼容是硬骨头
最难的不是路由,是数据库。v2 改了订单表结构,加了个 status_detail 字段。我们遵循"可回滚的变更顺序":
- 先加字段(nullable,旧版忽略它),灰度跑一周;
- 新字段写入由 v2 负责,旧版读时不报错;
- 确认无回滚需求后,再清理旧逻辑。
反过来,删字段必须先让所有版本都不读它。这个顺序一旦乱,回滚就丢数据。我们吃过一次亏:有次先删了旧字段又回滚,老版本读不到直接 NPE。从此数据库变更单独走评审,和代码发布解耦,且必须先在预发环境演练回滚。
回滚机制
灰度期间保留一键切流能力。监控发现 v2 错误率超过 0.5%,30 秒内把 X-Canary 路由权重调 0:
# 直接改网关权重,不重建实例
curl -XPOST gateway/route/order-v2 -d '{"weight":0}'
因为数据层是兼容的,切流即回滚,不用停服。我们一次真实故障里用这招把影响面从全量压到 0.3% 用户,运营甚至没感知到。回滚后 v2 实例保留,方便事后上去抓现场,而不是立刻缩容把证据销毁。
放量节奏与监控
我们定的放量节奏是 1% → 5% → 20% → 50% → 100%,每档观察至少 30 分钟,看错误率、P99、业务指标(下单成功率)。任何一档异常就停在当前档或回退。这个节奏比"发一半看一眼"严谨得多,也让我们在 5% 那档就抓到了一个偶发的缓存穿透。
小结
灰度发布不是"发一半"那么简单。染色解决"谁能进",路由解决"去哪",数据兼容解决"回得来",回滚解决"出事怎么办"。四者缺一不可。建议先用 1% 流量跑通整套,再逐步放量到 5%、20%、100%,每一步都靠监控数据说话,而不是靠"应该没问题"的直觉。灰度体系上线后,我们的发布事故率从每季度 2 次降到 0。