一次发布,五个服务里有三个改了代码。团队"一起发"——结果订单服务先起来,而它依赖的"库存服务新接口"还没上,于是下单全量失败了两分钟。复盘时说"以后按顺序发",这个"没有结论"很常见,因为顺序看起来是"经验问题"(谁先谁后有讲究)。而实际上它是"兼容性方向"的推论——一旦写出规则,就不必每次讨论。
而"顺序"是什么、凭什么定,没有结论。
多模块/多服务的发布顺序不是"经验",而是由"兼容性方向"决定:先发"能被旧版调用"的那一方,再发"开始使用新接口"的那一方。这个规则能覆盖绝大多数情况,而它是可推导的。
1. 单个改动的"两端"
| 端 | 角色 |
|---|---|
| 提供方(Provider) | 新增/修改接口 |
| 消费方(Consumer) | 调用新接口 |
顺序固定:先发布"提供方的新版本(同时兼容旧调用)",再发布"消费方的新版本"。这样任何时刻"新旧混跑"都能工作。
反过来做就是"消费方先发"——于是它调用一个还不存在的接口,直接失败。第二项(改语义)不能靠顺序解决:两个版本的"同一接口"含义不同,无论谁先发,混跑期都会有一方理解错。所以它必须"并存两套",而不是"排个顺序"。
而这次失败会持续到"提供方发布完成",即两分钟到两小时。
2. 三种兼容策略与发布顺序
| 策略 | 顺序 |
|---|---|
| 加字段/加接口(向后兼容) | 提供方先发,消费方后发 |
| 改语义(不兼容) | 需"并行版本"三步走 |
| 删接口(破坏性) | 消费方先迁移,最后删 |
第二项不能靠"顺序"解决,只能靠"扩展-收缩":先并存两套(新旧都支持),再迁移消费方,最后删旧的。
第三项的"证据"要求最严:"删接口"是不可逆的,而"我以为没人用"是错觉的高发区——所以它必须靠"按接口统计调用量"而不是"记忆"。
第三项的顺序与第一项相反:消费方先停止使用,提供方才能删——而"停止使用"要靠"确认无调用"(日志统计),不能靠"我记得没有了"。
3. "一起发"为什么必然出问题
| 原因 | 说明 |
|---|---|
| 发布不是原子 | 实例逐个替换,中间态存在 |
| 回滚不是原子 | 回滚后又是另一个中间态 |
| 健康检查滞后 | 新实例"就绪"不等于"依赖可用" |
第一项最根本:滚动发布天然产生"新旧并存"的窗口第一项的"推论"是"任何一次发布都必须考虑混跑"——包括"回滚":回滚也是"新旧并存",只是方向相反。所以"回滚方案"也要按"混跑安全"设计。
,于是"必须考虑混跑"。而"一起发"的隐含假设是"要么全新、要么全旧"——这个假设不成立。
// 滚动发布的中间态(3 实例,依次替换)
t0: [旧][旧][旧]
t1: [新][旧][旧] ← 混跑:新调旧、旧调新,都要能工作
t2: [新][新][旧] ← 混跑
t3: [新][新][新]
// 结论:任何一次发布都要保证"t1、t2 的状态可用"
4. 三步走的具体做法
| 步骤 | 内容 |
|---|---|
| 1 扩展 | 提供方同时支持新旧,消费方不变 |
| 2 迁移 | 消费方切到新接口,旧路径保留 |
| 3 收缩 | 确认无调用后,提供方删旧 |
第三步的"确认"必须有数据:按接口/字段统计调用量,连续 N 天为 0。而"我看日志里没有"不足以支撑"删"。
// 发布单里"顺序"的显式表达(三行)
step_1: 库存服务 v2.3(新增 /stock/lock,兼容旧 /stock/deduct)
step_2: 订单服务 v1.9(切到 /stock/lock,保留 fallback 到 /stock/deduct)
step_3: 库存服务 v2.4(28 天后删 /stock/deduct,需 0 调用证据)
"混跑安全"要用"双向兼容"检查一遍,而不是只看"新调旧":
// 三项检查(发布前,逐项过)
1 新提供方 + 旧消费方:旧调用还能成功吗(接口/字段/语义)
2 旧提供方 + 新消费方:新调用会被拒绝吗(若会,不能先发消费方)
3 数据层:新旧两个版本读写同一份数据时,格式都兼容吗
// 第 3 项最容易被漏:代码兼容了,数据格式不兼容
5. 怎么定"整个发布"的顺序
| 方法 | 说明 |
|---|---|
| 按依赖图拓扑排序 | 提供方在前 |
| 按"改动类型"分组 | 兼容类先、不兼容类后 |
| 按"回滚影响"排序 | 影响小的先 |
第一项在"图有环"时失效:A 调 B、B 调 A(哪怕是间接),拓扑排序无解。此时要么"合并发布",要么"用扩展-收缩把环打断"。
第一项是"机械可算"的:把"调用关系"画成有向图,拓扑排序即可。而它需要"依赖图是最新的"——这在微服务里通常靠"服务注册 + 调用链数据"。
第三项是"风险控制":先发"回滚代价小"的服务,于是"出问题时只回滚一个"。第三项还有一个"观测"理由:把"影响面小"的服务先发,可以先用它验证"新版本的构建/配置/依赖"是否正常——于是"大问题"在"小服务"上先暴露。
"回滚"也要按"混跑安全"设计,而它的顺序是"发布的逆序":
// 一次发布与回滚的完整顺序(依赖图:库存 ← 订单)
发布:库存 v2.3(兼容)→ 订单 v1.9(用新接口)→ 库存 v2.4(删旧接口)
回滚:库存 v2.4 → v2.3(先恢复兼容)→ 订单 v1.9 → v1.8 → 库存不动
// 关键:删旧接口那一步回滚不了 → 所以"删"应当"至少晚 28 天",与发布分离
6. 排错:五种"发布顺序问题"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 发布后立刻大量报错 | 消费方先发 | 按"提供方先"重排 |
| 部分实例异常 | 混跑窗口不兼容 | 加"双版本兼容" |
| 回滚后又出错 | 回滚顺序错 | 按逆序回滚 |
| 删接口后仍有调用 | 未确认无调用 | 先统计再删 |
| 间歇性失败 | 依赖版本未对齐 | 核对同批版本矩阵 |
第四项的"实现"要能"按版本"统计:同一个接口,v1 与 v2 的调用量要分开。否则"总量在降"但"旧版仍有调用"会被掩盖。
第四项很常见:"我以为没人用" 与"确实没人用"之间,差一次"按接口统计调用量"。
"顺序"的落地形式是"发布单里的三段",它让"顺序"从"口头约定"变成"可检查的记录":
// 发布单(多服务改动时必填)
changes:
- svc: inventory ver: 2.3 type: add-api # 兼容
- svc: order ver: 1.9 type: use-new-api
- svc: inventory ver: 2.4 type: remove-api # 破坏性,28 天后
order: [[email protected], [email protected]] # 本批实际发布的顺序
deferred: [[email protected]] # 明确"不在本批"
rollback: "逆序回滚到 2.3/1.8,2.4 未发布故无需回滚"
7. 边界:什么情况顺序无所谓
| 场景 | 为什么 | 做法 |
|---|---|---|
| 服务间无调用(独立功能) | 无依赖 | 并行发 |
| 全程向后兼容(只加字段) | 混跑安全 | 并行发 |
| 单体应用(一次部署) | 无跨服务依赖 | 顺序无关 |
| 有"服务网格"做路由金丝雀 | 可按流量切 | 顺序可宽松 |
判断口径一句话:这次改动会不会让"新旧两个版本"在混跑时无法工作。最后一条与"组织"有关的边界:"发布顺序"必须有人负责。因为"每个服务各自发布"时,没人看"整体顺序"——于是"顺序"在所有团队都做对的情况下依然可能是错的。
会(改协议、改语义、删接口),就必须定顺序;不会(只加字段),并行发是安全的。
8. 自测
- 发布顺序由什么决定?为什么"提供方先发"能让混跑安全?
- "改语义"为什么不能靠顺序解决?"扩展-收缩"三步分别做什么?
- "一起发"为什么必然出问题?滚动发布的中间态是什么?
- 第三步"收缩"需要什么证据?为什么"我看日志里没有"不够?
- "按回滚影响排序"的作用是什么?
- 判断"顺序是否重要"的那个问题是什么?