两个团队同时改接口,联调时才发现各改了一半。复盘通常落在"沟通不够"上,但沟通只是表象:真正的问题是顺序——没人说过谁先动、谁在过渡期承担什么、什么时候算完。契约变更的协同不是文档工作,是一次带时序的工程协调,顺序错了,再多沟通也只是把返工推到联调那天。
1. 协同问题本质上是顺序问题
一个契约两侧各有一份实现,变更意味着两份实现要先后落到不同状态。任意时刻只可能出现三种组合,其中两种是"半改"。
| 状态 | 提供方 | 消费方 | 结果 |
|---|---|---|---|
| 旧×旧 | 旧契约 | 旧契约 | 正常 |
| 新×旧 或 旧×新 | 一侧已改 | 一侧未改 | 只在兼容时正常 |
| 新×新 | 新契约 | 新契约 | 正常 |
协同要做的就是安全地穿过中间状态抵达第三种。问题不是"要不要改",是"中间状态怎么活"。
2. 先改提供方,还是先改消费方
两条路径都成立,选择依据是"哪一侧的中间状态更安全",而不是哪边更闲。
| 顺序 | 适用条件 | 中间状态 | 风险 |
|---|---|---|---|
| 先改提供方 | 提供方能同时输出新旧两种形态 | 新×旧:旧消费方读旧字段 | 提供方要维护双写 |
| 先改消费方 | 消费方能同时兼容新旧两种形态 | 旧×新:消费方容忍缺失或未知 | 消费方要维护双读 |
| 不可双向兼容 | 语义变化,两侧必须同刻切换 | 中间态不安全 | 只能靠原子切换或停机窗口 |
口诀:能容忍未知的一侧先改。消费方若已实现宽松解析,可先在旧契约上适配新形态,等提供方切换时不再改动。
3. 四种变更模式与各自的失败方式
模式没有优劣,失败方式各异;选模式等于选要防哪种事故。
| 模式 | 做法 | 失败方式 |
|---|---|---|
| 兼容扩展 | 只加可选字段,两侧都不改代码 | 被误判为安全,实际改了语义 |
| 双版本并行 | 新旧路径并存,按版本分流 | 旧版本永久存活,无人收尾 |
| 消费方先行适配 | 消费方先兼容两种形态 | 适配代码长期留着,变成第二套契约 |
| 同时切换 | 同刻上线,原子生效 | 无回滚路径,切换失败即全断 |
最容易出事的是"同时切换":它看起来最省事,实际上是唯一要求"零误差"的模式。
4. 过渡期的三种状态与必须存在的观测点
过渡期必须能被看见,否则没人知道进行到哪一步,也没人敢宣布结束。三个观测点缺一不可。
| 状态 | 观测点 | 判断依据 |
|---|---|---|
| 旧契约 | 旧字段/旧路径的调用量 | 该数归零才能进下一步 |
| 双契约 | 新形态调用量占比 | 占比爬升曲线是否健康 |
| 新契约 | 旧形态残留调用方列表 | 列表为空或只剩已同意豁免者 |
// 三状态观测的最小查询(语法按所用平台核对)
// 1) 旧形态是否还有人调
// sum(rate(api_calls_total{shape="legacy"}[5m]))
// 2) 新形态占比
// sum(rate(api_calls_total{shape="new"}[5m])) / sum(rate(api_calls_total[5m]))
// 3) 残留调用方
// sum by (client_id) (rate(api_calls_total{shape="legacy"}[1h]))
观测点要在变更之前就位——事后补埋点,往往正好错过双契约期最需要判断的那几周。
5. "同时切换"唯一可行的前提
同时切换要求两侧在同一时刻生效,只有两种工程手段能做到:同一发布单元,或可原子回滚。
| 前提 | 检查方式 | 不成立时 |
|---|---|---|
| 同一发布单元 | 两侧代码同仓、同流水线、同一次发布 | 改用双版本并行 |
| 可原子回滚 | 回滚脚本已演练,回滚耗时已知 | 先建回滚路径再谈切换 |
| 状态可共存 | 数据层能同时容纳新旧语义 | 先做数据迁移与双写 |
| 无跨版本缓存 | 缓存键含语义版本 | 清缓存或加版本前缀 |
// 同时切换的最小回滚契约:切换前必须写好并演练
switch:
at: "CUTOVER_TS"
steps: [ "停旧写入", "双写校验", "切读", "观察 OBSERVE_WINDOW", "停旧读" ]
rollback:
trigger: "错误率 > ERROR_THRESHOLD 或 关键接口 P99 > LATENCY_THRESHOLD"
action: "回切读路径 + 保留双写"
tested_at: "REHEARSAL_DATE"
6. 谁承担过渡期成本
这条不谈清楚,协同必然演变成推诿。过渡期有三类成本,必须逐项指定承担方。
| 成本 | 具体形态 | 常见默认(危险) |
|---|---|---|
| 双写/双读实现 | 一方或双方多维护一条路径 | "谁提需求谁做" → 提出方往往只提不实现 |
| 观测与告警 | 新形态的看板与阈值 | "观测是平台的事" → 没人认领 |
| 清理 | 删除临时兼容代码 | "以后再说" → 永久保留 |
可执行的规则:变更提出方承担过渡期的观测与清理,被变更方只承担自己一侧的适配。把清理责任写进同一张工单,而不是留在口头。
7. 公告该写什么
公告不是礼貌,是接口。四件事缺一件,对方就做不到独立完成迁移。
| 要素 | 内容 | 缺失后果 |
|---|---|---|
| 改什么 | 字段/路径/语义的具体差异 | 对方按猜测改,改错方向 |
| 何时 | 生效日与旧形态停止日 | 无限期拖延 |
| 影响谁 | 按调用方列出的受影响清单 | 长尾调用方最后才发现 |
| 怎么验证 | 一条可执行的验证命令或对照用例 | 迁移完成的判定变成主观 |
// 公告模板:四要素 + 一个可执行的验证方式
// 变更:POST /orders 的 amount 单位由「元」改为「分」,字段名与类型不变
// 生效:EFFECTIVE_DATE 起双读;SUNSET_DATE 起仅返回分
// 影响:调用方 CLIENT_A、CLIENT_B(其余调用方未使用该字段)
// 验证:curl -s APP_URL/orders/SAMPLE_ID | jq '.amount' 应与下单金额×100 一致
8. 变更窗口与发布窗口的协调
两侧各有自己的发布节奏,协同失败常常不是技术问题,而是窗口错位:一方周五发,另一方下周二才发,中间跨了一个周末。
| 情形 | 处理 |
|---|---|
| 两侧发布窗口不重叠 | 先做兼容态发布(一侧能容忍两种形态),再各自按窗口上 |
| 一侧有变更冻结期 | 冻结期前完成兼容态,冻结期只做切换 |
| 跨时区团队 | 以"最后一个窗口关闭"为对齐点,而不是以第一个为准 |
| 紧急修复需求 | 允许绕过窗口,但必须走同一套兼容态要求 |
一句话原则:窗口不一致时,先让系统进入兼容态,再各自发。兼容态是唯一能在窗口之间安全停留的状态。
9. 验收判据
"改完了"不是判据。可验收的判据只有三条,都能用数回答。
| 判据 | 阈值形式 | 采集方式 |
|---|---|---|
| 旧调用量归零 | 旧形态请求数持续为 0 达 STABLE_WINDOW | 按 shape 标签统计 |
| 新调用成功率达标 | 成功率不低于变更前的同口径基线 | 分组对比 |
| 错误率无变化 | 变更前后错误率差在噪声范围内 | 同窗口对照 |
// 验收用一条命令跑完,不靠人肉对看板
// 1) 旧形态残留
curl -s PROM_URL/api/v1/query --data-urlencode 'query=sum(rate(api_calls_total{shape="legacy"}[1h]))'
// 2) 新形态成功率与基线差
curl -s PROM_URL/api/v1/query --data-urlencode 'query=sum(rate(api_ok_total{shape="new"}[1h]))/sum(rate(api_calls_total{shape="new"}[1h]))'
─── 三个数都达标才算完成,缺一个就只能算"已切换,未验收"
10. 临时兼容代码的清理责任与截止日
过渡期代码是技术债的定时器。没有截止日,它就变成永久契约的一部分,且没人记得为什么存在。
| 项 | 要求 | 反模式 |
|---|---|---|
| 责任 | 具名到人,不是团队 | "后端组会清" |
| 截止日 | 与旧形态下线日同一张工单 | "迁移完成后" |
| 标记 | 代码里带 TODO(CONTRACT-xxx) 与日期 |
无标记,靠记忆 |
| 复核 | 到期未清则在例会上被点名 | 到期无人提,静默转永久 |
// 临时兼容代码必须自带截止日与工单号,否则不会被自动追缴
// TODO(CONTRACT-1234, deadline=EXPIRE_DATE) 旧字段兜底,旧形态下线后删除
if (request.getLegacyAmount() != null) {
amount = request.getLegacyAmount(); // 单位:元,仅旧调用方使用
}
11. 用什么判断协同效率
| 度量 | 定义 | 健康信号 |
|---|---|---|
| 契约变更周期 | 公告日到验收完成日 | 有上限且稳定 |
| 过渡期长度 | 进入双契约到旧形态归零 | 逐次缩短 |
| 跨团队返工次数 | 因顺序错误导致的重复改动 | 趋零 |
| 遗留兼容代码数 | 带 CONTRACT 标记未清的处数 | 到期即递减 |
返工次数是最诚实的指标:它直接反映顺序是否被讲清楚,而不受个人沟通能力影响。
12. 排错:协同失败的常见形态
| 现象 | 原因 | 处理 |
|---|---|---|
| 联调才发现各改一半 | 没有约定顺序,双方各自开工 | 先定兼容态,再定谁先发 |
| 双契约期拖了一个季度 | 旧形态没人观测,不知道还剩谁 | 变更前就位观测,按调用方点名 |
| 切换当天全断 | 走"同时切换"但无回滚路径 | 补回滚演练,或改双版本并行 |
| 兼容代码越堆越多 | 无截止日与责任 | 加标记、工单与到期复核 |
| 公告发了但没人动 | 公告缺验证方式,对方不知从何开始 | 补一条可执行的验证命令 |
13. 边界:什么时候协同流程该压到一句话
| 情形 | 判断依据 | 做法 |
|---|---|---|
| 同一团队内部 | 同一批人、同一发布节奏 | 口头对齐 + 一条群消息 |
| 调用方可同步升级 | 同一发布单元内 | 一起发,不做兼容态 |
| 仅新增可选字段 | 旧调用方不改也不出错 | 一句通知即可 |
| 内部实验接口 | 生命周期以周计,无外部依赖 | 直接改,不公告 |
反过来,一旦调用方跨团队、跨公司、或难以协调发布窗口,上面这套顺序与责任就必须写下来——省掉的每一条,都会在联调那天以返工的形式出现。
14. 自测
- 契约变更的协同问题,本质上是哪一类问题?为什么"多沟通"解决不了它?
- "先改提供方"与"先改消费方"的选择依据是什么?有没有一个更简的判断口诀?
- 四种变更模式里,哪一种要求"零误差"?它可行的两个工程前提是什么?
- 过渡期三个观测点分别是什么?为什么必须在变更之前就位?
- 过渡期成本该由谁承担?请给出一个可执行的分配规则。
- 验收判据有哪三条?缺一条时最可能的后果是什么?