《Java 消息中间件·事件驱动架构》15.2 故障演练剧本

故障演练的核心不是出了故障再临时应对,而是在系统正常运转时主动模拟各类故障场景并验证恢复能力。本文给出 broker 挂、消费者挂、投影错、契约破坏四个剧本的完整执行步骤、环境要求、回滚方案与检查清单,用纯 JDK 模拟演练自动化检查器与改进工单生成流程。

演练不是测试,是彩排

测试验证的是「系统能不能工作」,演练验证的是「系统坏了之后能不能恢复」。测试的范围是正常场景,演练的范围是异常场景;测试的目标是发现问题,演练的目标是验证恢复能力。

四个剧本覆盖事件驱动系统最常见的四类故障:

剧本故障类型影响范围恢复时间目标
剧本一Broker 挂生产/消费全部中断< 30 秒(自动切换)
剧本二消费者挂消费堆积,投影滞后< 5 分钟(重启消费者)
剧本三投影数据错读模型数据不一致< 1 小时(重放重建)
剧本四契约破坏消费者解析失败< 15 分钟(回滚或升级)

每个剧本包含四个要素:触发条件、执行步骤、预期结果、回滚方案。

剧本一:Broker 挂

触发条件:模拟主 broker 进程被 kill,集群进入无主状态。

执行步骤:

  1. 在测试环境 kill leader broker 进程
  2. 观察 controller 是否能在 10 秒内选举新 leader
  3. 观察生产者是否能在新 leader 选举后继续发送
  4. 观察消费者是否能在新 leader 选举后继续消费

预期结果:

  • 分区 leader 在 10 秒内切换到 follower
  • 生产者在 request timeout 内重试成功
  • 消费者重新连接新 leader,消费继续
  • 无数据丢失(acks=all 且 minISR 大于 1)

回滚方案:启动被 kill 的 broker 进程,让它重新加入集群作为 follower。

剧本二:消费者挂

触发条件:模拟消费者组内所有实例同时崩溃。

执行步骤:

  1. 停止消费者组的所有实例
  2. 生产者继续发送事件(验证事件不丢)
  3. 等待 5 分钟(模拟故障窗口)
  4. 重启消费者实例

预期结果:

  • 消费者停止期间,事件堆积在 topic 中
  • 重启后消费者从上次提交的 offset 继续消费
  • 投影数据在追赶 lag 后恢复一致
  • lag 在 10 分钟内归零

回滚方案:消费者本身就是回滚——重启实例即可恢复。如果重启后的数据不一致,需要从历史事件重放。

剧本三:投影数据错

触发条件:模拟投影消费者写入错误数据(如金额字段被错误放大 10 倍)。

执行步骤:

  1. 在投影消费者中注入 bug:把 amount 字段乘以 10
  2. 让 bug 运行 10 分钟(产生错误数据)
  3. 发现错误后,停止消费者,修复 bug
  4. 从错误发生前的 offset 开始重放

预期结果:

  • 错误数据只在投影表中,源事件数据正确
  • 重放后投影表数据与源事件一致
  • 重放期间的读服务可用(但可能读到旧数据)
  • 重放完成后数据一致性校验通过

回滚方案:投影数据错误的回滚不是「把错误数据改回去」,而是「把正确数据重写一遍」。重放是标准做法。

剧本四:契约破坏

触发条件:模拟生产者发布了一个新 schema,但消费者尚未升级。

执行步骤:

  1. 生产者添加一个新字段 regionCode(非可选)
  2. 旧消费者尝试解析新消息
  3. 观察消费者的异常率和死信队列数量
  4. 消费者升级后,观察异常是否消失

预期结果:

  • 旧消费者遇到新字段时抛异常
  • 异常消息进入死信队列(如果配置了 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 时挂掉,恢复行为完全不同。演练时需要保持持续的生产-消费流量。

从演练到生产保护的转化

演练发现的问题需要转化成生产保护措施。转化方式有三类:

  1. 监控告警:演练中发现 broker 切换时控制器选举超过 10 秒,转化成生产环境的「controller 选举时间告警」,阈值 5 秒。

  2. 自动恢复:演练中发现消费者重启后 lag 需要 10 分钟才能恢复,转化成「自动扩容消费者」策略——lag 超过阈值时自动启动备用实例。

  3. 流程改进:演练中发现 schema 升级时缺少消费者兼容性检查,转化成「schema 变更审批流程」——所有 schema 变更必须经过兼容性自动检测。

转化的关键是把演练中的「时间点」变成生产中的「阈值」。演练记录了「broker 切换花了 8 秒」,生产的告警阈值就设在 5 秒——留出3秒的预警窗口。

演练频率与范围

演练不是一次性的,需要定期执行:

演练类型频率范围耗时
桌面演练每月讨论流程,不实际执行1 小时
自动演练每季度自动化脚本随机 kill broker/消费者2 小时
全链路演练每半年完整执行四个剧本1 天
红蓝对抗每年红队随机制造故障,蓝队恢复3 天

桌面演练最便宜,但效果有限——只验证了「人知道怎么做」,没验证「系统能不能做到」。全链路演练成本最高,但效果最好。推荐组合:每月桌面演练加每季度自动演练。

演练的常见误区

演练最容易陷入的五个误区:

误区一:「演练过了就等于准备好了」

演练通过只能说明「在演练的条件下能恢复」,不说明「在真实故障时能恢复」。演练环境的流量是模拟的,真实的故障可能伴随着网络分区、磁盘满、GC 停顿等复合问题。演练后还需要分析「演练条件和真实条件的差距」,列出仍依赖真实环境验证的假设。

误区二:「自动化演练太低效」

自动化演练确实不如红蓝对抗有戏剧性,但它能做到「每周一次」甚至「每天一次」。红蓝对抗半年一次已经成本很高。自动化演练积累的数据(平均恢复时间、失败率趋势)比单次对抗更有参考价值。

误区三:「演练越复杂越好」

有些团队把多个故障同时注入,如「挂了三个 broker 加一个磁盘满加一个网络分区」。这种复合故障虽然更接近真实,但出现根因时无法定位——到底是哪个故障导致了最终失败?演练应该从单故障开始,逐步叠加。

误区四:「演练结果不用记录」

演练结果不记录,三个月后没人记得上次演练发现了什么问题。演练报告应该包含:日期、剧本、发现的问题、改进工单编号、负责人、复测日期。

误区五:「只演练技术故障」

工程师倾向于演练技术故障(broker 挂),但业务故障(如「投影数据错」)可能更常见。业务故障的演练需要业务方参与,技术团队单独做效果有限。

演练产出是改进工单

演练的最终产出不是「通过了四个剧本」,而是「发现了三个需要改进的地方」。每个未通过项都要生成一个改进工单:

未通过项改进动作负责人截止日期
lag 容忍度不足增加 3 个备用消费者实例SRE1 周内
投影数据不一致建立定时一致性校验任务开发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 全书验收

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《Java 消息中间件·事件驱动架构》附录 B 架构决策记录模板
  2. 《Java 消息中间件·事件驱动架构》附录 A 术语表
  3. 《Java 消息中间件·事件驱动架构》15.3 全书验收