故障演练与混沌工程:故障注入、爆炸半径、稳态指标与恢复验证
故障演练解决的是“系统真的遇到下游超时、注册中心抖动、Redis变慢、MQ堆积、数据库锁等待、实例重启、机房切换时,保护机制是否按预期工作”。混沌工程不是随机把生产弄坏,而是在明确假设、稳态指标、爆炸半径、停止条件和回滚方案下,主动注入受控故障,验证系统的韧性。
相关基础可继续阅读:稳定性治理、容量规划、服务发现、多机房与异地多活、可观测性。
学习目标
学完本页要能回答:
- 故障演练、故障注入、混沌工程、灾备演练分别是什么。
- 为什么只做单元测试、压测和灰度还不够。
- 演练前怎样定义假设、稳态指标、爆炸半径、停止条件和回滚方案。
- 常见故障怎么注入:延迟、错误、丢包、断连、CPU、内存、磁盘、实例重启、下游限流。
- 怎样验证超时、重试、熔断、限流、降级、Bulkhead、幂等、补偿是否真实有效。
- 为什么写请求故障演练必须带幂等和事实查询。
- 演练后怎样形成缺陷、修复、复演和知识库沉淀。
一、为什么必须做故障演练
架构图里的保护机制,只有在故障发生时才知道是不是真的有效。
flowchart TD
A["设计超时、熔断、限流、降级"] --> B["正常测试通过"]
B --> C["真实下游变慢"]
C --> D{"保护是否按预期生效"}
D -- "是" --> E["故障被限制在小范围"]
D -- "否" --> F["线程池、连接池、重试和队列放大事故"]常见误判:
| 以为 | 实际风险 |
|---|---|
| 配了超时就安全 | 超时比入口SLO还长,入口已经失败,后台仍执行 |
| 配了重试就更可靠 | 多层重试把下游流量放大数倍 |
| 配了熔断就不会雪崩 | 错误分类不对,慢调用不计入,HALF_OPEN恢复洪峰 |
| 配了降级就没影响 | 降级返回伪成功,破坏业务事实 |
| 有监控就能发现 | 指标维度不对,无法按租户、接口、实例定位 |
故障演练的目标不是证明系统完美,而是尽早暴露缺口。
二、基本概念
| 概念 | 含义 | 例子 |
|---|---|---|
| 故障注入 | 人为制造一个受控故障 | 让库存接口延迟2秒 |
| 混沌实验 | 有假设、有指标、有爆炸半径的故障注入 | 验证单实例下游慢不会拖垮订单 |
| 灾备演练 | 验证重大故障切换和恢复 | 主机房不可用后切到灾备 |
| 稳态指标 | 判断系统是否仍健康的指标 | 成功率、P99、错误率、订单创建量 |
| 爆炸半径 | 故障影响范围 | 一个实例、一个租户、1%流量 |
| 停止条件 | 何时立即终止实验 | 错误率超过1%、P99超过2s |
混沌工程不是“越刺激越好”。真正成熟的演练往往从小范围、低风险、可回滚开始。
三、标准演练流程
flowchart TD
A["提出假设"] --> B["定义稳态指标"]
B --> C["限定爆炸半径"]
C --> D["准备停止条件和回滚"]
D --> E["注入故障"]
E --> F["观察指标和业务事实"]
F --> G{"是否符合预期"}
G -- "是" --> H["记录证据并扩大或结束"]
G -- "否" --> I["立即停止并恢复"]
I --> J["修复缺口"]
J --> K["复演验证"]每次演练前必须写清:
| 项 | 示例 |
|---|---|
| 假设 | 库存服务延迟2秒时,订单服务会在300ms内失败或降级,不拖满线程池 |
| 稳态 | 下单成功率大于99%,P99小于800ms,线程池队列小于100 |
| 范围 | 只对灰度租户、1个调用方实例、5%流量 |
| 故障 | 给库存查询增加2秒延迟,持续10分钟 |
| 停止条件 | 订单错误率超过1%,P99超过2秒,线程池拒绝超过阈值 |
| 回滚 | 移除故障规则,恢复配置,必要时摘流灰度实例 |
| 负责人 | 执行人、观察人、回滚人、业务确认人 |
四、常见故障注入类型
| 故障类型 | 注入方式 | 验证什么 |
|---|---|---|
| 延迟 | 下游接口 sleep、代理延迟、tc netem | 超时、Deadline、Bulkhead |
| 错误 | 返回500、业务错误码、异常 | 错误分类、熔断、降级 |
| 连接失败 | 拒绝连接、目标端口关闭 | connect timeout、有限重试 |
| 读超时 | 接收请求但不返回 | read timeout、UNKNOWN处理 |
| 丢包抖动 | 网络丢包、延迟、重排 | 重试、连接池恢复 |
| CPU打满 | stress工具或限额 | HPA、降级、实例摘除 |
| 内存压力 | 内存占用、GC变长 | 健康检查、熔断、OOM预警 |
| 磁盘满 | 日志盘、数据盘写满 | 日志限速、降级、告警 |
| 实例重启 | kill pod、滚动发布 | 服务发现、连接排空、优雅下线 |
| MQ堆积 | 降低消费速率或制造失败 | Lag、死信、扩容和背压 |
| Redis变慢 | 增加延迟、阻塞命令 | 缓存降级、回源保护 |
| DB锁等待 | 人为持锁或慢SQL | 连接池、超时、限流 |
五、微服务关键演练清单
5.1 下游慢调用
目标:验证慢依赖不会拖垮上游。
要看:
- 上游线程池 active、queue、reject。
- 下游连接池 pending、active。
- P95/P99 是否在 SLO 内。
- 熔断是否按慢调用或失败率打开。
- 降级响应是否符合业务语义。
- 停止故障后 HALF_OPEN 是否有限探测。
5.2 多层重试
目标:验证不会出现 Gateway、Feign、HTTP Client、业务代码同时重试。
flowchart TD
A["原始1次请求"] --> B["Gateway重试2次"]
B --> C["Feign重试3次"]
C --> D["HTTP Client重试2次"]
D --> E["最坏12次物理尝试"]演练时要统计逻辑请求数和物理 attempt 数。如果物理 attempt 远高于预期,说明重试 Owner 没收敛。
5.3 写请求超时
目标:验证写请求状态未知时不会重复副作用。
流程:
flowchart TD
A["创建订单请求"] --> B["下游提交成功"]
B --> C["响应被延迟或丢失"]
C --> D["调用方得到超时"]
D --> E["按幂等号查询事实"]
E --> F["确认成功、失败或待处理"]禁止演练中直接用新请求号再次提交。写请求故障演练必须提前准备幂等键、查询接口、补偿表和对账脚本。
5.4 注册中心不可用
目标:验证运行中调用方使用本地快照能短时间工作,但新实例发现和下线传播受影响。
要看:
- 已有调用是否继续成功。
- 新消费者启动是否失败。
- 新实例是否无法被发现。
- 旧实例摘除是否延迟。
- 本地快照年龄和 No instance 次数。
5.5 MQ堆积和恢复
目标:验证堆积时不会丢消息、不会无限重试、恢复后不会打垮下游。
要看:
- Lag 总量和 Lag 分布。
- 单分区或单队列是否热点。
- 消费重试和死信数量。
- 下游 DB/Redis/ES 是否成为瓶颈。
- 扩容消费者后是否受分区数限制。
- 恢复阶段是否限速,避免瞬时追赶打爆下游。
六、爆炸半径设计
演练必须从小到大。
| 阶段 | 范围 | 适合故障 |
|---|---|---|
| 本地/测试环境 | 单机、Mock依赖 | 基础错误分类、超时 |
| 预发环境 | 接近生产链路 | 注册中心、MQ、DB、Redis |
| 生产单实例 | 一个调用方或一个提供方实例 | 连接池、熔断、下线 |
| 生产灰度租户 | 指定租户或医院 | 真实业务闭环 |
| 生产小比例流量 | 1% 或 5% | 稳态验证 |
| 全链路灾备 | 受审批控制 | 机房切换、RPO/RTO |
不能一上来全站断 Redis、断数据库、杀全部实例。那不是混沌工程,是制造事故。
七、观测证据
演练必须留下证据。
| 证据 | 说明 |
|---|---|
| Trace | 请求是否到达下游、attempt次数、目标实例 |
| Metrics | QPS、P95/P99、错误率、限流、熔断、线程池、连接池 |
| Logs | 关键错误、降级原因、幂等号、traceId |
| 业务事实 | 订单、支付、库存、消息、ES、缓存是否一致 |
| 控制面 | 注册中心、配置中心、规则版本是否生效 |
| Runbook记录 | 开始时间、停止时间、影响范围、恢复动作 |
没有证据的演练只能算“感觉没出事”。生产级演练要能回答:故障什么时候注入、影响了谁、保护器何时生效、业务事实是否正确、恢复用了多久。
八、JDK 8 Demo:演练停止条件
下面 Demo 模拟一个最小停止条件判断器。
public class ChaosStopConditionDemo {
static final class Metrics {
final double errorRate;
final long p99Ms;
final int queueSize;
Metrics(double errorRate, long p99Ms, int queueSize) {
this.errorRate = errorRate;
this.p99Ms = p99Ms;
this.queueSize = queueSize;
}
}
static boolean shouldStop(Metrics metrics) {
return metrics.errorRate > 0.01
|| metrics.p99Ms > 2000
|| metrics.queueSize > 100;
}
public static void main(String[] args) {
Metrics healthy = new Metrics(0.002, 600, 20);
Metrics dangerous = new Metrics(0.03, 2500, 180);
System.out.println("healthyStop=" + shouldStop(healthy));
System.out.println("dangerousStop=" + shouldStop(dangerous));
}
}输出:
healthyStop=false
dangerousStop=true真实系统不能只靠人工盯屏。演练平台或脚本应该自动读取指标,触发停止条件后立即撤销故障规则并告警。
九、商业场景
9.1 医院接口变慢
假设:某医院接口从 300ms 变成 5s 时,只影响该医院采集任务,不拖垮其他医院。
验证:
- 该医院采集线程池是否满。
- 其他医院采集是否正常。
- 医院维度熔断是否打开。
- 失败任务是否进入可补偿状态。
- MQ Lag 是否只在该医院维度增长。
9.2 订单库存服务故障
假设:库存服务 500 或超时时,下单接口明确失败或返回待确认,不会伪成功,不会重复扣库存。
验证:
- 订单状态是否保持合法状态机。
- 幂等号重复提交是否复用结果。
- 重试次数是否符合预算。
- 库存服务恢复后 HALF_OPEN 是否平滑。
9.3 ES同步失败
假设:ES 写入失败不会影响 MySQL 主事务,失败事件进入死信或重试,后续能按 MySQL 重建。
验证:
- MySQL 事实源是否提交。
- Outbox/CDC 是否有事件。
- ES 消费失败是否可见。
- 重放后 ES 版本是否追平。
十、演练后复盘
演练结束不是“恢复了就完了”。要形成闭环:
| 输出 | 说明 |
|---|---|
| 事实时间线 | 故障注入、指标变化、保护生效、恢复时间 |
| 影响面 | 租户、接口、实例、消息、数据 |
| 缺陷列表 | 配置错误、监控缺失、Runbook缺失、代码问题 |
| 修复计划 | Owner、截止时间、验证方式 |
| 复演记录 | 修复后同类故障再次验证 |
| 文档更新 | 更新知识库、值班手册、面试素材 |
十一、面试标准回答
混沌工程不是随机断生产,而是在明确假设、稳态指标、爆炸半径、停止条件和回滚方案下做受控故障注入。比如验证库存服务延迟2秒时,订单服务是否按Deadline超时、Bulkhead是否限制线程占用、熔断是否按慢调用打开、降级是否符合业务语义、写请求是否按幂等号查询事实。演练范围要从测试、预发、生产单实例、灰度租户逐步扩大,观察Trace、P99、错误率、线程池、连接池、熔断状态、业务流水和消息状态。演练后要修复缺口并复演,而不是只证明“这次没挂”。
十二、本章小结
分布式系统的可靠性不是配置出来的,而是被验证出来的。超时、重试、熔断、限流、降级、幂等、补偿、对账、灾备,只有经过受控故障演练,才能证明在真实失败下能保护业务。
