演练不是测试,是彩排
测试验证的是「系统能不能工作」,演练验证的是「系统坏了之后能不能恢复」。测试的范围是正常场景,演练的范围是异常场景;测试的目标是发现问题,演练的目标是验证恢复能力。
四个剧本覆盖事件驱动系统最常见的四类故障:
| 剧本 | 故障类型 | 影响范围 | 恢复时间目标 |
|---|---|---|---|
| 剧本一 | Broker 挂 | 生产/消费全部中断 | < 30 秒(自动切换) |
| 剧本二 | 消费者挂 | 消费堆积,投影滞后 | < 5 分钟(重启消费者) |
| 剧本三 | 投影数据错 | 读模型数据不一致 | < 1 小时(重放重建) |
| 剧本四 | 契约破坏 | 消费者解析失败 | < 15 分钟(回滚或升级) |
每个剧本包含四个要素:触发条件、执行步骤、预期结果、回滚方案。
剧本一:Broker 挂
触发条件:模拟主 broker 进程被 kill,集群进入无主状态。
执行步骤:
- 在测试环境 kill leader broker 进程
- 观察 controller 是否能在 10 秒内选举新 leader
- 观察生产者是否能在新 leader 选举后继续发送
- 观察消费者是否能在新 leader 选举后继续消费
预期结果:
- 分区 leader 在 10 秒内切换到 follower
- 生产者在 request timeout 内重试成功
- 消费者重新连接新 leader,消费继续
- 无数据丢失(acks=all 且 minISR 大于 1)
回滚方案:启动被 kill 的 broker 进程,让它重新加入集群作为 follower。
剧本二:消费者挂
触发条件:模拟消费者组内所有实例同时崩溃。
执行步骤:
- 停止消费者组的所有实例
- 生产者继续发送事件(验证事件不丢)
- 等待 5 分钟(模拟故障窗口)
- 重启消费者实例
预期结果:
- 消费者停止期间,事件堆积在 topic 中
- 重启后消费者从上次提交的 offset 继续消费
- 投影数据在追赶 lag 后恢复一致
- lag 在 10 分钟内归零
回滚方案:消费者本身就是回滚——重启实例即可恢复。如果重启后的数据不一致,需要从历史事件重放。
剧本三:投影数据错
触发条件:模拟投影消费者写入错误数据(如金额字段被错误放大 10 倍)。
执行步骤:
- 在投影消费者中注入 bug:把 amount 字段乘以 10
- 让 bug 运行 10 分钟(产生错误数据)
- 发现错误后,停止消费者,修复 bug
- 从错误发生前的 offset 开始重放
预期结果:
- 错误数据只在投影表中,源事件数据正确
- 重放后投影表数据与源事件一致
- 重放期间的读服务可用(但可能读到旧数据)
- 重放完成后数据一致性校验通过
回滚方案:投影数据错误的回滚不是「把错误数据改回去」,而是「把正确数据重写一遍」。重放是标准做法。
剧本四:契约破坏
触发条件:模拟生产者发布了一个新 schema,但消费者尚未升级。
执行步骤:
- 生产者添加一个新字段 regionCode(非可选)
- 旧消费者尝试解析新消息
- 观察消费者的异常率和死信队列数量
- 消费者升级后,观察异常是否消失
预期结果:
- 旧消费者遇到新字段时抛异常
- 异常消息进入死信队列(如果配置了 DLQ)
- 升级后消费者正常处理
- 无数据丢失(消息在 DLQ 中保留)
回滚方案:短期是消费者升级到新版本;长期是建立 schema 兼容性门禁,阻止破坏性变更直接上线。
演练检查器 Java 模拟
import java.util.*;
public class DrillChecker {
record Drill(String name, boolean brokerBackup, boolean lagTolerable,
boolean projectionConsistent, boolean schemaCompatible) {}
static List<String> check(Drill d) {
List<String> issues = new ArrayList<>();
if (!d.brokerBackup()) issues.add("broker 备份不可用");
if (!d.lagTolerable()) issues.add("lag 容忍度不足");
if (!d.projectionConsistent()) issues.add("投影数据不一致");
if (!d.schemaCompatible()) issues.add("schema 不兼容");
return issues;
}
public static void main(String[] args) {
List<Drill> drills = List.of(
new Drill("剧本一: Broker挂", true, true, true, true),
new Drill("剧本二: 消费者挂", true, false, true, true),
new Drill("剧本三: 投影错", true, true, false, true),
new Drill("剧本四: 契约破坏", true, true, true, false)
);
System.out.println("=== 故障演练检查 ===");
int pass = 0;
for (Drill d : drills) {
List<String> issues = check(d);
if (issues.isEmpty()) {
System.out.println("✓ " + d.name() + ": 通过");
pass++;
} else {
System.out.println("✗ " + d.name() + ": 未通过 - " + String.join(", ", issues));
}
}
System.out.println("\n通过: " + pass + "/" + drills.size());
System.out.println("演练结论: " + (pass == drills.size() ? "全部通过,可投产" : "存在未准备项,需修复后复测"));
}
}
本机实测:
javac --release 21 DrillChecker.java && java DrillChecker
预期输出:
=== 故障演练检查 ===
✓ 剧本一: Broker挂: 通过
✗ 剧本二: 消费者挂: 未通过 - lag 容忍度不足
✗ 剧本三: 投影错: 未通过 - 投影数据不一致
✗ 剧本四: 契约破坏: 未通过 - schema 不兼容
通过: 1/4
演练结论: 存在未准备项,需修复后复测
检查器输出直接告诉我们哪些问题需要修复。每个「未通过」项对应一个明确的准备动作:lag 容忍度不足对应增加备用消费者实例,投影数据不一致对应建立数据一致性校验脚本,schema 不兼容对应升级消费者或建立兼容性门禁。
演练的环境要求
演练不能直接在生产环境执行——四个剧本中的任意一个都会导致真实故障。演练需要搭建专用的演练环境,环境要求如下:
| 要求 | 说明 | 验证方式 |
|---|---|---|
| 数据隔离 | 演练环境的数据不能影响生产 | 独立的数据库实例和网络 |
| 规模对等 | 演练环境的拓扑和生产一致 | 相同的分区数、副本因子、消费者数 |
| 流量模拟 | 演练环境需要模拟生产流量 | 用压测工具生成接近生产的消息速率 |
| 可恢复性 | 演练环境崩溃不影响生产 | 演练完成后可以重置或销毁 |
规模对等是最容易被忽略的。很多团队在只有 3 个 broker 的演练环境里测试 10 个 broker 生产集群的故障场景,结果完全不同。10 个 broker 挂 1 个,分区 leader 可以在其他 9 个之间切换;3 个 broker 挂 1 个,如果 ISR 不足,分区可能进入不可用状态。
流量模拟也需要注意。空跑 broker 挂的测试没有意义——broker 在有大量 inflight 请求时挂掉,和 idle 时挂掉,恢复行为完全不同。演练时需要保持持续的生产-消费流量。
从演练到生产保护的转化
演练发现的问题需要转化成生产保护措施。转化方式有三类:
监控告警:演练中发现 broker 切换时控制器选举超过 10 秒,转化成生产环境的「controller 选举时间告警」,阈值 5 秒。
自动恢复:演练中发现消费者重启后 lag 需要 10 分钟才能恢复,转化成「自动扩容消费者」策略——lag 超过阈值时自动启动备用实例。
流程改进:演练中发现 schema 升级时缺少消费者兼容性检查,转化成「schema 变更审批流程」——所有 schema 变更必须经过兼容性自动检测。
转化的关键是把演练中的「时间点」变成生产中的「阈值」。演练记录了「broker 切换花了 8 秒」,生产的告警阈值就设在 5 秒——留出3秒的预警窗口。
演练频率与范围
演练不是一次性的,需要定期执行:
| 演练类型 | 频率 | 范围 | 耗时 |
|---|---|---|---|
| 桌面演练 | 每月 | 讨论流程,不实际执行 | 1 小时 |
| 自动演练 | 每季度 | 自动化脚本随机 kill broker/消费者 | 2 小时 |
| 全链路演练 | 每半年 | 完整执行四个剧本 | 1 天 |
| 红蓝对抗 | 每年 | 红队随机制造故障,蓝队恢复 | 3 天 |
桌面演练最便宜,但效果有限——只验证了「人知道怎么做」,没验证「系统能不能做到」。全链路演练成本最高,但效果最好。推荐组合:每月桌面演练加每季度自动演练。
演练的常见误区
演练最容易陷入的五个误区:
误区一:「演练过了就等于准备好了」
演练通过只能说明「在演练的条件下能恢复」,不说明「在真实故障时能恢复」。演练环境的流量是模拟的,真实的故障可能伴随着网络分区、磁盘满、GC 停顿等复合问题。演练后还需要分析「演练条件和真实条件的差距」,列出仍依赖真实环境验证的假设。
误区二:「自动化演练太低效」
自动化演练确实不如红蓝对抗有戏剧性,但它能做到「每周一次」甚至「每天一次」。红蓝对抗半年一次已经成本很高。自动化演练积累的数据(平均恢复时间、失败率趋势)比单次对抗更有参考价值。
误区三:「演练越复杂越好」
有些团队把多个故障同时注入,如「挂了三个 broker 加一个磁盘满加一个网络分区」。这种复合故障虽然更接近真实,但出现根因时无法定位——到底是哪个故障导致了最终失败?演练应该从单故障开始,逐步叠加。
误区四:「演练结果不用记录」
演练结果不记录,三个月后没人记得上次演练发现了什么问题。演练报告应该包含:日期、剧本、发现的问题、改进工单编号、负责人、复测日期。
误区五:「只演练技术故障」
工程师倾向于演练技术故障(broker 挂),但业务故障(如「投影数据错」)可能更常见。业务故障的演练需要业务方参与,技术团队单独做效果有限。
演练产出是改进工单
演练的最终产出不是「通过了四个剧本」,而是「发现了三个需要改进的地方」。每个未通过项都要生成一个改进工单:
| 未通过项 | 改进动作 | 负责人 | 截止日期 |
|---|---|---|---|
| lag 容忍度不足 | 增加 3 个备用消费者实例 | SRE | 1 周内 |
| 投影数据不一致 | 建立定时一致性校验任务 | 开发 | 2 周内 |
| schema 不兼容 | 升级消费者到 v2.1 | 开发 | 1 周内 |
改进工单必须包含可验证的完成标准。例如「增加备用消费者」的完成标准是「单消费者故障后 lag 在 5 分钟内归零」,不是「增加了实例」。
演练报告的标准模板
每次演练结束后,报告必须包含以下字段,否则三个月后没人记得演练的结论:
| 字段 | 内容 | 示例 |
|---|---|---|
| 演练日期 | 执行的日期和时间 | 2026-10-15 14:00 |
| 剧本名称 | 四个剧本之一 | 剧本二:消费者挂 |
| 执行人 | 谁触发了故障 | 张三 |
| 观察人 | 谁记录了系统行为 | 李四、王五 |
| 故障注入方式 | 具体怎么模拟的故障 | kill consumer group 全部 3 个实例 |
| 故障持续时间 | 故障保持了多长时间 | 5 分钟 |
| 观察结果 | 系统的实际行为 | lag 从 0 增长到 45000,重启后 8 分钟恢复 |
| 预期结果 | 剧本中定义的通过标准 | lag 在 10 分钟内归零 |
| 是否通过 | 观察结果 vs 预期结果 | ✓ 通过 |
| 新发现问题 | 剧本中未覆盖的问题 | 消费者重启时触发 rebalance,lag 额外增长 2 分钟 |
| 改进工单 | 需要跟进的事项 | TICKET-2047:增加 rebalance 期间的 readonly 模式 |
| 复测日期 | 下次验证的日期 | 2026-10-22 |
模板的价值在于标准化和可追溯性。不同的人执行同一剧本,如果报告格式不一致,比较结果会很困难。模板强制所有人记录同样的字段,让跨团队的演练评审成为可能。
本节判据
- 演练不是测试,是验证系统在故障后的恢复能力。
- 四个剧本覆盖 broker 挂、消费者挂、投影错、契约破坏四类常见故障。
- 每个剧本必须包含触发条件、执行步骤、预期结果、回滚方案四个要素。
- 演练检查器输出明确的未通过项,每项对应一个改进工单。
- 推荐演练频率:每月桌面演练加每季度自动演练加每半年全链路演练。
- 本机实测:四个剧本中三个未通过,对应 lag 容忍度、投影一致性、schema 兼容性三个改进项。
- 改进工单必须有可验证的完成标准,不能是模糊描述。
- 演练的产出是改进项清单,不是「通过/不通过」的简单结论。每次演练都应该让系统比上一次更可靠,形成持续改进的闭环。
演练的持续改进闭环是系统可靠性工程的核心实践,值得每个事件驱动团队投入资源建立。
阅读导航:上一节 15.1 SLO 与容量 | 下一节 15.3 全书验收
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。