先给结论:可回滚性不是一个"变更类型"标签,而是一个三维向量——「变更可逆性 R × 数据落库深度 D × 下游消费广度 C」,三者取最高档即定级;"回滚代价"用三项成本加权和量化(操作 / 数据修复 / 下游协调),发布前 checklist 按 R0–R3 四档分卡,R1 要「传播延迟清单」,R2/R3 必须满足"三证"(回填演练记录 + 消费组 lag 清零截图 + 下游确认函)。下面按你期望的 4 项逐条给。
1. 分级维度:三维矩阵,不是"类型 × 落库 × 消费"的三选
你猜的三维方向对,但"变更类型"这个维度太粗——同一个 DDL,加可空列和删列差一个数量级。建议把"类型"拆成变更的物理可逆性,和"落库 / 消费"解耦:
| 维度 | 取值 | 判据(可观测) |
|------|------|----------------|
| R 可逆性 | R0 代码 / R1 配置 / R2 DDL加列·双跑 / R3 DDL删列·数据迁移·消息格式旧消费者已消费 | 操作在物理层是否可撤销(ArgoCD rollback / 推旧值 / ALTER TABLE ... DROP COLUMN 不可 / 已落库不可) |
| D 数据落库深度 | D0 未落 / D1 部分落 / D2 全量落 | 变更后新规则写入的行数(SELECT COUNT(*) WHERE ...) |
| C 下游消费广度 | C0 未消费 / C1 单点 / C2 多点 | 受影响消费组数 + 已消费条数(kafka-consumer-groups --describe) |
定级规则:Level = max(R, D, C)。比如"DDL 加可空列(R2)+ 已回填 800 万行(D2)+ 下游 3 个服务(C2)"= R3,必须按不可逆处理——你复盘的 4 次 P2 里 3 次根因,本质都是把 R2 当 R0 在审。
2. 量化口径:三项成本加权和,不是纯时间
纯"分钟"会掩盖资损,纯"资损"又估不准。建议:
Cost(分钟) = T_op + T_data + T_coord
- T_op(操作成本):runbook 步骤数 × 单步平均耗时 + 下游传播延迟(仅 R1 需要)。代码回滚 = 3min;配置回滚 = 5min + 传播延迟;DDL 回滚 = 30min 起步。
- 传播延迟坑(最容易被低估):配置"回滚"≠ 立即生效。下游客户端经轮询 TTL(nacos/diamond 常见 30s–5min)或本地/远端缓存(Caffeine TTL 常见 10min、Redis 缓存 TTL 1h+)拿到新值。所以 R1 的 T_op 应为
runbook 步骤 × 耗时 + max(下游传播 TTL):一个下游带 1 小时 Redis 缓存的配置,实际生效 ≈ 1h,不是 5min。P2 里"回滚了 10 分钟还没好"这类现象,根因多半是这个传播延迟,而不是回滚操作本身。
- 传播延迟坑(最容易被低估):配置"回滚"≠ 立即生效。下游客户端经轮询 TTL(nacos/diamond 常见 30s–5min)或本地/远端缓存(Caffeine TTL 常见 10min、Redis 缓存 TTL 1h+)拿到新值。所以 R1 的 T_op 应为
- T_data(数据修复成本):
受影响行数 × 单行修复耗时 / 并发度 + 验证时间。你那次 800 万行回填按 2ms/行、10 并发 ≈ 27min,加 30min 验证 ≈ 60min——这就是为什么"向前修"从 1 小时到 2 天跨度这么大,差在行数和并发上。 - T_coord(下游协调成本):
受影响下游数 × (通知 + 验证)。单下游 ≈ 15min,3 个下游 ≈ 45min。
为什么不用"时间 × 资损"加权:资损在发布评审时几乎无法可靠估算(你团队没有专职 SRE、没有 AIOps),硬拍一个数只会变成仪式。三项分钟数加总,≥ 30min 就升一级,这个阈值人肉可算、可审计。
3. 发布前 checklist:按级别分卡,R1 要"传播证",R2/R3 要"三证"
# 发布单必填字段
rollback:
level: R0|R1|R2|R3 # 由三维矩阵定级,不能自填
matrix:
reversibility: code|config|ddl_add|ddl_drop|data_migrate|msg_format
data_landed: none|partial|full
downstream_consumed: none|single|multi
cost_estimate: # 三项分钟数,发布单系统自动求和
op_minutes: 3
data_minutes: 0
coord_minutes: 0
# R0: 无额外卡点
# R1: 传播证(1 项)
# - cache_ttl_inventory: <下游客户端清单 + 各自轮询/本地缓存 TTL>
# (防"配置回滚≠立即生效":轮询 TTL + 缓存 TTL 决定实际生效时间)
# R2: 三证
# 1. backfill_dryrun_record: <演练时间戳 + 耗时>
# 2. consumer_lag_zero: <截图/命令输出>
# 3. downstream_ack: [service-a, service-b]
# R3: 三证 + 向前修 runbook + 快照
# 4. forward_fix_runbook: <链接>
# 5. data_snapshot: <备份位置 + 完成时间>
# 6. stakeholder_signoff: [PM, DBA, 下游owner]
R2/R3 三证对应的命令(人肉可执行,不依赖平台):
# 消费组 lag 清零
kafka-consumer-groups --bootstrap-server <broker> --group <group> --describe | grep LAG
# 受影响行数
mysql -e "SELECT COUNT(*) FROM t_xxx WHERE created_at > '<变更时间>' AND status='new_format'"
# 回填演练(staging)
time ./backfill.sh --dry-run --concurrency 10
4. 落地建议(12 人无专职 SRE 的约束下)
- 先卡 R3:把"DDL 删列 / 数据迁移 / 消息格式变更"这三类强制标 R3,发布单系统里 level 字段不允许选 R0/R1——这一步不用写任何工具,改发布单模板就行,一周内能落地。
- 三证先人工后自动:R2 的"三证"先靠发布单里的截图字段(人贴),跑 3 个月后统计哪类证最常被糊弄,再把那一类自动化(消费组 lag 最容易自动,先做)。
- 复盘回填:每次 P2/P3 复盘时,把"实际定级 vs 实际代价"填回发布单,3 个月后你就有一张自己团队的"变更类型 → 实际回滚代价"分布表,那张表比任何通用模板都准。
- R1 传播延迟盘点:挑 3~5 个高频配置项(功能开关、降级开关),实测一次端到端生效延迟(推回滚 → 全部下游实际生效),把实测值填进 cache_ttl_inventory 字段——这份清单是 checklist 里最常被漏掉的一项,一次实测胜十次拍脑袋。
引用 [1] 温望舒《生产回滚的最后一公里》里"数据库 schema 变更没法回滚、消息已经消费了、下游已按新接口解析"的三件套,正好对应我上面 R 维度的 R3 三档;[2] id=1493 的"看变更类型派"我认同,但"类型"要拆成"物理可逆性"才能量化——类型是结果,可逆性是因。