裁掉一个字段的决策做了两天,实施花了一小时,回滚花了两周——因为数据已经被清理,下游有三个系统在读它。事后复盘时有人说"当时评估过风险",但评估的是"做不做得到",没有人评估"退不退得回来"。
技术决策的质量通常按"对不对"来衡量,但在不确定的环境里,"错了能不能改"往往比"对不对"更重要。可逆的错决定成本很低,不可逆的对决定也可能因为环境变化而变成灾难。
1. 可逆性的四个等级
| 等级 | 回滚方式 | 时间成本 |
|---|---|---|
| R1 配置级 | 改配置重启 | 分钟 |
| R2 代码级 | 回滚发布 | 十分钟到小时 |
| R3 数据级 | 从备份或双写恢复 | 小时到天 |
| R4 契约级 | 需要通知并协调所有相关方 | 天到周 |
关键区别在"谁必须参与回滚":R1 与 R2 由自己团队决定,R3 需要数据可用,R4 需要外部配合。级别的判定依据不是技术难度,而是参与方数量。
等级还会随时间漂移:一个当时是 R2 的决定,半年后会变成 R4——因为依赖它的系统变多了。所以可逆性不是决策时的一个静态属性,而是需要定期重估的状态;决策时是 R2,文档里写着 R2,实际已经 R4,是最常见的一类误判。
第三、四级的分界最容易被误判:删掉一个内部接口看起来是 R2,但如果它被别的团队调用,就是 R4。
2. 不可逆动作的前置投入
| 不可逆动作 | 前置投入 | 收益 |
|---|---|---|
| 删字段 | 先停止写入并观察一个周期 | 确认无人使用 |
| 删接口 | 先加弃用告警与日志 | 拿到调用方清单 |
| 改语义 | 新增参数/字段,旧路径保留 | 双跑对比 |
| 数据清理 | 先归档再做删除 | 可从归档回填 |
| 迁移库 | 双写与双向校验 | 可切回 |
前置投入的收益可以用一句话概括:把"不可逆动作"变成"可逆动作 + 一段观察期"。观察期的成本通常是几天到两周,而不可逆动作失败的成本是数周到数月。
第二项是成本最低的:弃用告警只需要在接口入口记一条日志,跑两周就能拿到真实的调用方清单,而这份清单是不可逆动作的必要前提。
// 弃用的三个动作,顺序不能反
1. 加告警:记录调用方标识与调用量,保留 2 周
2. 通知:按清单逐一确认迁移计划
3. 下线:调用量归零后保留 1 周再删除
// 直接跳到第 3 步,等于赌"没人用"
3. 双写与灰度是可逆性的载体
| 手段 | 提供的可逆性 | 代价 |
|---|---|---|
| 双写 | 可切回,且可对比 | 两套写入的一致性处理 |
| 灰度 | 影响面可控 | 需要分流能力 |
| 特性开关 | 秒级关闭 | 开关本身需要治理 |
| 影子(只读) | 验证读路径 | 下游压力翻倍 |
第三项最便宜,也最容易腐化:开关数量增长后,没人知道哪个开关还是活的。可逆性因此变成"名义上可逆"——关闭开关会引发另一处未知行为。
开关的另一种腐化形态是嵌套:if (flagA && flagB) 这样的组合出现后,实际生效路径变成四种,而没有人验证过四种组合都正确。
开关治理的最小要求是三条:有归属人、有到期时间、有定期清理。缺一条,它就只是"看起来能关"。
4. 回滚预案要测
| 预案要素 | 检查方式 |
|---|---|
| 回滚命令存在 | 演练执行一次 |
| 回滚所需权限可用 | 非本人也能执行 |
| 回滚时间在窗口内 | 计时 |
| 数据能回到一致状态 | 校验比对 |
| 回滚后监控正常 | 观察一个周期 |
演练的产出不只是一次计时,更是一份"缺口清单":
// 一次回滚演练应产出的五项记录
1. 实际耗时:__ 分钟(对比窗口 __ 分钟)
2. 卡住的位置:__(缺权限/缺脚本/缺数据)
3. 需要补充的权限授予:__
4. 校验手段是否足够:__(能否证明回到一致状态)
5. 下次演练的负责人与时间:__
第 2 项通常是最大的收获:多数团队的第一次演练都会在某一步卡住,而这一步就是真实事故中会多花一小时的环节。
第二项在事故现场最致命:预案在,权限只在个别人手里,而那个人不在。可逆性不是文档属性,是可用性属性。
// 回滚演练的最小脚本形态:每一步都留时间戳
start=$(date +%s)
./scripts/rollback.sh --to "$PREV_VERSION"
echo "rollback $(( $(date +%s) - start ))s"
./scripts/verify.sh --expect "$PREV_VERSION"
// 只有"执行 + 校验 + 计时"三步都跑过,预案才算存在
5. 什么时候该主动放弃可逆性
| 场景 | 为什么 | 前提 |
|---|---|---|
| 可逆性成本高于错误代价 | 双写维护成本 > 一次重建 | 数据可重建 |
| 短生命周期系统 | 赶不上一次回滚 | 影响面小 |
| 有强外部约束(合规删除) | 必须不可逆 | 流程与记录到位 |
| 试验性功能 | 结果本就未知 | 有隔离与开关 |
还有一个与决策流程有关的观察:可逆性越高的方案,越应该快速决定;可逆性越低的方案,越应该慢下来。现实中常常相反——容易改的方案被反复讨论,难改的方案反而因为"反正要慎重"而匆匆拍板。
第一项的判断方式是算一笔账:为了保住可逆性,你要长期维护两套路径。如果这个维护成本已经超过"出错后重做的成本",那么可逆性是负收益。
双写也不是万能的,它自己会产生两个问题:一致性与维护成本。前者的处理方式是"以一侧为准 + 定时对账",后者的处理方式是设一个明确的结束时间。
// 双写的三个约束,缺一个它就会长期挂着
1. 主从关系明确:以哪一侧为准,另一侧只做校验
2. 对账机制:每日比对差异,差异率与阈值告警
3. 退出条件:校验连续 N 天为 0,则停止旧路径写入
// 缺第 3 条的双写会一直存在,成为永久的维护负担
6. 排错:五种"以为能退实际退不回"
| 现象 | 成因 | 处理方向 |
|---|---|---|
| 回滚后数据不一致 | 只有单向写入 | 补双写或补对账 |
| 回滚导致下游报错 | 下游已按新契约改动 | 契约类变更需协调 |
| 关开关没效果 | 开关已被硬编码覆盖 | 开关治理与清理 |
| 回滚超时 | 从未计时 | 演练并记录 |
| 回滚权限不足 | 权限集中在个别人 | 至少两人可用 |
第一项最典型:切换到新路径后只写新表,回滚时新表的数据回不去。所以可逆性的前提是"数据能双向流动",而不只是"代码能切回"。
7. 边界:什么情况不必评估可逆性
| 场景 | 为什么 | 做法 |
|---|---|---|
| 纯新增(无人依赖) | 删掉即可 | 直接做 |
| 本地开发环境改动 | 无外部影响 | 不必评估 |
| 已有完整数据重建能力 | 错误可重放 | 评估重建时间即可 |
| 极小内部工具 | 影响面为零 | 不评估 |
// 决策单上的一行:把可逆性写成可查的字段
reversibility:
level: R3
how: "从日备恢复被删数据,需 4 小时"
who: "DBA 张(备选:李)"
window: "4h,业务可接受停机 6h"
reevaluate_at: 2027-03 // 依赖增长后可能已降级
判断口径一句话:错了之后,需要几个团队配合才能退回来。零个,可逆性由自己掌握;一个以上,就必须在决策时把回滚路径写下来。
8. 自测
- 可逆性四个等级的判定依据是什么?为什么不是技术难度?
- 为什么"删内部接口"可能是 R4 而不是 R2?判断依据是什么?
- 弃用告警为什么是不可逆动作的必要前提?三个动作的正确顺序是什么?
- 特性开关为什么会从"可逆性载体"退化为"名义上可逆"?开关治理的最小三条要求是什么?
- 回滚预案的五个要素中,哪一项在事故现场最致命?为什么?
- "可逆性是负收益"的判断方式是什么?它的前提条件是什么?