清单怎么用
这不是一张"跑一遍就打勾"的表。它的用法是:每次改动涉及复制、分片、事务、消息或锁时,从六类场景里选出受影响的两到三类,按表准备、注入、观测、判定、回滚。
使用前先确认三个前提:
- 每个场景都有明确的业务不变量,且不变量能写成断言;
- 注入手段可停止、作用范围可控;
- 观测信号能在同一条时间轴上对齐。
缺任何一条,验证都会退化为"看起来恢复了"。附录 A 给模型与故障语义,本附录给可执行步骤,两者配合使用。
共用前置:六类场景都要准备
| 项目 | 要求 | 不满足时的后果 |
|---|---|---|
| 环境 | 与生产拓扑同构,至少节点角色一致 | 结论无法外推 |
| 数据 | 有基线快照,可重置,可重复 | 两次结果不可比 |
| 观测 | 指标、日志、追踪可对齐到毫秒 | 无法判断因果 |
| 注入 | 有开关、有范围、有停止手段 | 演练变成事故 |
| 基线 | 注入前记录正常指标 | 无法判断是否恢复 |
| 回滚 | 每步有命令、有验证、有预计耗时 | 故障时手忙脚乱 |
| 记录 | 专人记录时间线,不参与操作 | 复盘时间线失真 |
| 审批 | 影响范围、时间窗、负责人明确 | 越权操作 |
数据准备容易被低估。跨分片与对账场景需要可构造的"漏单、重复、金额差、状态差",如果只能等真实流量出现,验证就无法按计划执行。正确做法是准备一组可重置的订单与流水样本,并在每次执行前恢复到已知状态。
判定不变量先写成断言
| 不变量 | 断言写法 | 检查方式 |
|---|---|---|
| 库存不超卖 | available >= 0 对所有 SKU 成立 | 全量扫描或增量校验 |
| 订单状态合法 | 每次转移属于允许边集合 | 状态机日志校验 |
| 支付不重复 | 每订单成功扣款次数 <= 1 | 流水聚合 |
| 无悬挂预留 | 预留超过补偿窗口必须已补偿或已释放 | 按时间筛选 |
| 资金守恒 | 扣款总额等于订单应付总额 | 对账任务 |
| 消息副作用一次 | 每 message_id 的业务效果计数 == 1 | 去重表与业务表比对 |
断言要能直接查库或查接口,不接受"人工看一眼"。人工判断只用于分类与处置,不用于判定通过与否。
场景 1:单主复制切换
| 项目 | 内容 |
|---|---|
| 前置条件 | 一主多从或共识组;复制模式(同步/异步)已知;客户端能发现新主;有切换脚本与基线快照 |
| 注入手段 | 停止主节点进程;隔离主节点网络;暂停主节点(模拟假死);注入复制延迟 |
| 观测信号 | 主从位点差、复制延迟、选主耗时、客户端错误率、双主写入迹象、未提交事务数 |
| 判定不变量 | 已确认提交的写不丢失;同一任期不出现两个接受写的节点;切换后读不倒退 |
| 通过标准 | 同步复制下 RPO 目标为 0;异步复制下丢失窗口不超过声明位点差;双主窗口为 0;RTO 在演练目标内 |
| 回滚方式 | 旧主恢复前先确认未被新主覆盖;以新主为准只读或重建;禁止双向合并 |
这张表里最容易写错的是"通过标准"。RPO 与 RTO 必须是团队自己确认的目标值,不能照抄别处的数字。同步复制声明 RPO 为 0,前提是提交确认确实等待了多数派;如果客户端在本地提交后就收到成功,故障切换仍可能丢尾部。
“假死"比"进程退出"更值得测。进程暂停时,健康检查可能仍认为它存活,选主不会立即发生;恢复后它可能带着旧任期继续写。判定要点不是"多久恢复”,而是"恢复后有没有被 fencing 拦住"。
场景 2:跨分片下单
| 项目 | 内容 |
|---|---|
| 前置条件 | 订单与库存位于不同分片;Saga 或 outbox 已接线;补偿接口幂等;可按订单号查询中间态 |
| 注入手段 | 库存预留成功后、订单提交前崩溃;订单提交超时;补偿消息重复投递;分片路由指向旧节点 |
| 观测信号 | 订单状态、预留记录、补偿记录、outbox 积压、悬挂时长、库存可用量 |
| 判定不变量 | 订单存在则预留存在或已补偿;任意 SKU 的 available >= 0;无超过补偿窗口的 INVENTORY_RESERVED |
| 通过标准 | 最终状态属于 PAID 或 CANCELLED;补偿重试后收敛;无重复扣减;悬挂时长为 0(目标值) |
| 回滚方式 | 停止新单入口;按订单号重放补偿;对账修复差异;确认无在途消息后再放开 |
跨分片场景的难点不是"失败",而是"不知道失败"。订单提交超时后,订单可能已创建,也可能没有;库存可能已预留,也可能没有。验证时必须覆盖四种组合,而不是只测"两边都成功"和"两边都失败"。
判定时先看订单,再看预留,最后看补偿记录。如果订单不存在但预留存在,说明补偿还没跑或丢了;如果订单存在但预留不存在,说明订单侧越过了预留直接提交。两种都是不变量破坏,不能只看最终库存数。
场景 3:重复消费
| 项目 | 内容 |
|---|---|
| 前置条件 | 消息至少一次投递;消费端有去重表或幂等键;副作用可查询;去重窗口大于最大重试间隔 |
| 注入手段 | 同一 message_id 重复投递(示例 3 次);副作用完成后、ack 前崩溃;重放位点回退 |
| 观测信号 | 去重命中次数、业务表行数、资金流水条数、ack 延迟、死信数量 |
| 判定不变量 | 每个 message_id 的业务副作用恰好一次;重复投递后聚合状态不变;流水总额等于业务总额 |
| 通过标准 | 重复 N 次与执行 1 次结果一致(示例 N=3);无重复流水;无因重复导致的库存多扣 |
| 回滚方式 | 消息无需回滚;修复去重表或业务数据后重放;明确记录窗口边界 |
重复消费的验证不能只看"第二次有没有报错"。正确做法是对比两次执行后的业务快照:订单状态、库存余量、流水条数、账户余额都必须一致。
还要测"副作用已发生但确认未发出"的窗口。消费者在写业务表和写去重表之间崩溃,恢复后消息重投,如果去重表尚未写入,副作用就会发生第二次。解决方式是把业务写入与去重写入放在同一本地事务,或者让业务操作本身带唯一约束。相关分析见 8.2 消费端幂等、去重表与恰好一次的真实边界 。
场景 4:锁过期
| 项目 | 内容 |
|---|---|
| 前置条件 | 锁有 TTL 与续约;带 fencing token;存储侧校验 token;被保护操作幂等或带条件写 |
| 注入手段 | 持锁进程暂停超过 TTL;注入长 GC 或网络暂停;新持有者接管后旧持有者恢复 |
| 观测信号 | 锁获取与释放日志、token 值、被拒绝的写次数、双写迹象、续约失败次数 |
| 判定不变量 | 过期持有者的写被拒绝;token 单调递增;同一资源不出现并发写成功 |
| 通过标准 | 旧持有者恢复后的写入 100% 被拒绝(目标值);无双重扣减;任一时刻至多一个有效持有者 |
| 回滚方式 | 取消旧持有者任务;以存储侧条件写结果为准;人工核对被拒绝的写 |
锁过期场景的核心不是"锁会不会过期",而是"过期之后旧持有者还能不能造成影响"。如果存储侧只认"当前锁的 key 存在",旧持有者恢复后可能在新持有者持锁期间写入;只有比较 fencing token 或版本号,才能拒绝陈旧写。
本场景必须与业务不变量一起判定。锁本身没有对错,只有"锁失效时业务是否仍然正确"。这和第 10.3 锁失效后的正确性:从互斥到幂等 的结论一致。
场景 5:时钟回拨
| 项目 | 内容 |
|---|---|
| 前置条件 | ID 生成器有回拨策略;事件时间与处理时间分离;TTL 与租约使用单调时钟;NTP 配置已知 |
| 注入手段 | 将节点时钟回拨(示例 5 分钟);冻结时钟;触发 NTP 大步调整;跨节点制造偏差 |
| 观测信号 | ID 唯一性与单调性、事件时间与处理时间差、租约有效期、证书与 token 过期异常 |
| 判定不变量 | 生成的 ID 不重复;订单状态不因时间倒退而回退;TTL 不提前失效也不永不过期 |
| 通过标准 | 回拨期间 ID 唯一性 100%(目标值);无重复 ID;无非法状态转移;恢复后自动追上 |
| 回滚方式 | 恢复系统时钟;暂停该节点发号;切换到备用 worker id 或号段;核对已发出的 ID |
回拨的影响面比想象中宽:Snowflake 类 ID 可能重复,基于时间的 TTL 可能提前过期,租约可能被误判失效,按时间排序的事件流可能乱序。验证时要分别检查这四条,不能只看 ID。
“示例 5 分钟"只是注入幅度,不代表真实环境的最大偏差。真实偏差取决于 NTP 配置、虚拟化时钟和运维操作;团队应记录自己环境的历史最大偏差,并据此设计回拨策略。
场景 6:对账收敛
| 项目 | 内容 |
|---|---|
| 前置条件 | 双方流水可导出;差异分类规则明确;对账任务幂等;有人工处置队列与 owner |
| 注入手段 | 构造漏单、重复、金额差、状态差各一笔;暂停消费一段时间;让一侧写入成功、另一侧超时 |
| 观测信号 | 差异数量与分类分布、收敛时长、自动修复率、人工队列积压、资金总额 |
| 判定不变量 | 收敛后差异集合为空,或全部有分类与 owner;资金总额守恒;修复操作幂等 |
| 通过标准 | 在声明的收敛窗口内差异清零(示例 T+1 或 1 小时,需业务确认);每笔差异有处置记录 |
| 回滚方式 | 对账任务可重跑;修复操作幂等;无法自动修复的进入人工,不直接改账 |
对账是最后一道防线,不是唯一防线。它验证的是"前面的机制漏了之后,系统能不能发现并收敛”,因此必须故意制造差异,而不是等差异自然出现。
判定时要区分"收敛"与"掩盖":如果对账任务把差异直接改成一致,却没有留下分类与原因,下一次同类问题仍会发生。通过标准里必须包含"差异可解释",否则只是把问题藏起来。
演练顺序:先单点,后组合
六类场景不要一次全上。推荐的顺序是先验证单点机制,再验证复制与分片,最后才做组合。原因很简单:多个故障叠加后,观测信号会互相污染,归因变得不可能。
| 阶段 | 场景 | 验证重点 | 进入下一阶段的条件 |
|---|---|---|---|
| 1 | 重复消费、锁过期 | 幂等与 fencing 是否真的生效 | 单场景下不变量成立 |
| 2 | 单主复制切换 | 丢失窗口、选主、双主窗口 | RPO/RTO 达到声明目标 |
| 3 | 跨分片下单 | 中间态、补偿、悬挂 | 无超过窗口的悬挂 |
| 4 | 时钟回拨 | ID 唯一性、TTL、状态顺序 | 无重复 ID 与非法转移 |
| 5 | 对账收敛 | 差异发现、分类、修复 | 差异可解释并清零 |
| 6 | 组合场景 | 故障相互放大时的行为 | 能定位到具体机制 |
组合场景一次只加一个变量。例如先做"复制切换 + 重复消费",确认消息在切换后重投仍幂等;再加"跨分片",观察补偿与切换是否冲突。一次叠加三个以上故障,通常只能得到"系统很乱"的结论。
数字口径:实测、目标与示例
附录里的每个数字都必须带口径,否则无法判断它能不能用于验收。
| 口径 | 含义 | 写法 | 用途 |
|---|---|---|---|
| 实测 | 在明确环境与命令下实际跑出的值 | 写明环境、版本、命令与样本 | 性能与容量结论 |
| 目标 | 设计要求达到的值 | 写明业务确认人与确认日期 | 验收与告警阈值 |
| 示例 | 仅用于说明量级或操作幅度 | 明确标注"示例" | 讲解与演练设计 |
| 无效 | 来源不明、环境不明、无法复现 | 不作为任何结论 | 应删除或重测 |
三条规则:
- 目标值不能从别处照抄,必须由业务与运维共同确认;
- 实测值必须附环境与命令,换机器、换版本后不能直接复用;
- 示例值不能进入验收标准,只能出现在讲解与注入设计里。
把"目标 RPO 为 0"写成"实测 RPO 为 0",是文档里最危险的错误之一。前者是承诺,后者是结论,两者需要的证据完全不同。
观测信号怎么对齐
验证失败最常见的原因不是系统坏了,而是信号对不上。下面五类信号要能在同一时间轴上关联:
| 信号 | 关键字段 | 对齐键 | 常见缺失 |
|---|---|---|---|
| 请求 | 时间、结果、耗时 | trace_id | 跨语言未透传 |
| 状态变更 | 变更前后状态、原因 | order_id | 只记结果不记原因 |
| 消息 | 生产、投递、消费、重投次数 | message_id | 未记录重投次数 |
| 锁 | 获取、续约、释放、拒绝 | 资源 + token | 只记成功不记拒绝 |
| 复制 | 位点、任期、延迟 | 节点 + 任期 | 位点含义混用 |
对齐不了时,先修观测再演练。凭日志猜时间线,得到的结论无法复现,也无法说服别人。
上线前 20 条检查
| 序号 | 检查项 | 证据 | 不通过时 |
|---|---|---|---|
| 1 | 每个核心业务不变量有书面定义 | 设计文档或 ADR | 不允许上线 |
| 2 | 不变量有自动化检查 | 校验任务或测试用例 | 补检查后再上线 |
| 3 | 幂等键作用域明确(租户/操作/请求) | 接口文档 | 重新设计键 |
| 4 | 去重窗口大于最大重试与重放间隔 | 配置与计算说明 | 扩大窗口或改策略 |
| 5 | 业务表与 outbox 同事务写入 | 代码与表结构 | 修复事务边界 |
| 6 | relay 位点持久化且可重放 | 表结构与恢复脚本 | 补位点 |
| 7 | 消费端去重写入与副作用原子 | 代码审查 | 改为同事务或唯一约束 |
| 8 | 只对幂等操作启用自动重试 | 重试配置清单 | 移除不安全重试 |
| 9 | 重试带指数退避与 jitter | 配置 | 补 jitter |
| 10 | 超时预算逐跳递减 | 调用链配置 | 重设预算 |
| 11 | 取消信号端到端传递 | 代码与压测 | 补传递 |
| 12 | 分布式锁带 fencing token | 锁实现与存储校验 | 补 token 校验 |
| 13 | 租约续约窗口小于 TTL | 配置与时间线 | 调整参数 |
| 14 | 耗时测量使用单调时钟 | 代码审查 | 改用单调源 |
| 15 | ID 生成器有回拨策略 | 设计与演练记录 | 补策略 |
| 16 | 复制切换的 RPO/RTO 已定义 | 运维文档 | 定义后再演练 |
| 17 | 分区时读写行为明确 | 设计文档 | 补降级方案 |
| 18 | 对账任务、告警、owner 就位 | 任务配置与值班表 | 补 owner |
| 19 | 六类场景至少各演练一次 | 演练记录 | 补演练 |
| 20 | 回滚步骤与数据兼容性已验证 | 回滚脚本与记录 | 修复后再上线 |
二十条里,1、2、12、14、19 最容易被跳过。它们不直接产生功能,却决定故障时能不能发现问题、能不能拒绝陈旧写、能不能复现结论。
通过、失败、无效三态
验证结论只有三种,不要用"基本通过"糊过去:
| 状态 | 判据 | 处理 |
|---|---|---|
| 通过 | 所有不变量成立,观测信号符合预期,回滚可执行 | 记录证据,进入下一场景 |
| 失败 | 任一不变量被破坏,或出现未解释的中间态 | 停止扩面,定位并修复后重跑 |
| 无效 | 注入未生效、观测缺失、环境与生产不同构 | 修复验证条件后重跑,不计入通过 |
“无效"最容易被误记为"通过”。例如注入的网络分区实际没有切断流量,服务自然一切正常;或者观测指标没有采集,只能凭日志猜测。这类结果必须标记为无效,否则会给出虚假的信心。
执行记录模板
每次验证至少记录以下字段:
场景:
目标不变量:
环境与版本:
注入手段与幅度:
开始时间 / 结束时间:
观测到的关键信号:
不变量检查结果:
结论:通过 / 失败 / 无效
失败或无效的原因:
回滚是否执行、耗时:
后续改进项与 owner:
记录要能支撑另一个工程师复现同一次验证。只写"切换成功"没有价值;写清"在什么拓扑、什么复制模式、什么注入幅度下,哪个不变量在什么时间点被检查、结果如何",才有价值。
场景选择:改了什么就测什么
不必每次跑满六类。按改动触及的机制选场景,既省时间,也不遗漏关键风险:
| 改动 | 至少覆盖 | 理由 |
|---|---|---|
| 复制模式或选主参数 | 单主复制切换 | 丢失窗口与双主窗口会变 |
| 分片键或路由表 | 跨分片下单、对账收敛 | 中间态与差异分类受影响 |
| 消费逻辑或去重表 | 重复消费 | 副作用边界改变 |
| 锁实现或 TTL | 锁过期 | fencing 与续约关系改变 |
| ID 生成器或 NTP 配置 | 时钟回拨 | 唯一性与 TTL 受影响 |
| 对账规则或修复脚本 | 对账收敛 | 收敛判定与幂等性改变 |
一次改动触及多条机制时,取并集,不要只选"最相关"的一条。例如同时改分片与消息消费,就要覆盖跨分片下单和重复消费,并额外做一次组合验证。
六个常见错误
第一,只测成功路径。跨分片下单如果只测"库存和订单都成功",永远发现不了悬挂。
第二,把超时当失败。超时是"未知",需要查询或幂等重试;直接判失败会掩盖已提交的副作用。
第三,注入不可停止。任何故障注入都必须有对应的停止手段和范围限制。
第四,没有基线。注入前不记录正常指标,事后无法判断"恢复了没有"。
第五,只验证最终数值,不验证中间态。最终库存正确,不代表过程中没有超卖;最终订单正确,不代表没有重复扣款。
第六,把一次通过当成永久结论。拓扑、版本、流量、配置变化后,原来的结论不再成立,需要重新验证。
小结
六类场景覆盖了卷三的主要风险面:复制切换看丢失窗口,跨分片下单看中间态,重复消费看副作用边界,锁过期看 fencing,时钟回拨看 ID 与 TTL,对账收敛看兜底能力。
执行时记住三句话:
- 先写不变量,再设计注入,最后才看指标;
- 目标值必须注明来源,实测值必须可复现;
- 通过、失败、无效三态分明,不用"基本正常"代替结论。
把这六类场景和二十条检查接进发布流程,比在事故后补文档更省成本。卷四会讨论如何把其中可自动化的部分平台化,但在那之前,先把每一次验证做成可重复、可追溯的记录。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。