Skip to content

故障演练与混沌工程:故障注入、爆炸半径、稳态指标与恢复验证

故障演练解决的是“系统真的遇到下游超时、注册中心抖动、Redis变慢、MQ堆积、数据库锁等待、实例重启、机房切换时,保护机制是否按预期工作”。混沌工程不是随机把生产弄坏,而是在明确假设、稳态指标、爆炸半径、停止条件和回滚方案下,主动注入受控故障,验证系统的韧性。

相关基础可继续阅读:稳定性治理容量规划服务发现多机房与异地多活可观测性

学习目标

学完本页要能回答:

  1. 故障演练、故障注入、混沌工程、灾备演练分别是什么。
  2. 为什么只做单元测试、压测和灰度还不够。
  3. 演练前怎样定义假设、稳态指标、爆炸半径、停止条件和回滚方案。
  4. 常见故障怎么注入:延迟、错误、丢包、断连、CPU、内存、磁盘、实例重启、下游限流。
  5. 怎样验证超时、重试、熔断、限流、降级、Bulkhead、幂等、补偿是否真实有效。
  6. 为什么写请求故障演练必须带幂等和事实查询。
  7. 演练后怎样形成缺陷、修复、复演和知识库沉淀。

一、为什么必须做故障演练

架构图里的保护机制,只有在故障发生时才知道是不是真的有效。

mermaid
flowchart TD
    A["设计超时、熔断、限流、降级"] --> B["正常测试通过"]
    B --> C["真实下游变慢"]
    C --> D{"保护是否按预期生效"}
    D -- "是" --> E["故障被限制在小范围"]
    D -- "否" --> F["线程池、连接池、重试和队列放大事故"]

常见误判:

以为实际风险
配了超时就安全超时比入口SLO还长,入口已经失败,后台仍执行
配了重试就更可靠多层重试把下游流量放大数倍
配了熔断就不会雪崩错误分类不对,慢调用不计入,HALF_OPEN恢复洪峰
配了降级就没影响降级返回伪成功,破坏业务事实
有监控就能发现指标维度不对,无法按租户、接口、实例定位

故障演练的目标不是证明系统完美,而是尽早暴露缺口。

二、基本概念

概念含义例子
故障注入人为制造一个受控故障让库存接口延迟2秒
混沌实验有假设、有指标、有爆炸半径的故障注入验证单实例下游慢不会拖垮订单
灾备演练验证重大故障切换和恢复主机房不可用后切到灾备
稳态指标判断系统是否仍健康的指标成功率、P99、错误率、订单创建量
爆炸半径故障影响范围一个实例、一个租户、1%流量
停止条件何时立即终止实验错误率超过1%、P99超过2s

混沌工程不是“越刺激越好”。真正成熟的演练往往从小范围、低风险、可回滚开始。

三、标准演练流程

mermaid
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 下游慢调用

目标:验证慢依赖不会拖垮上游。

要看:

  1. 上游线程池 active、queue、reject。
  2. 下游连接池 pending、active。
  3. P95/P99 是否在 SLO 内。
  4. 熔断是否按慢调用或失败率打开。
  5. 降级响应是否符合业务语义。
  6. 停止故障后 HALF_OPEN 是否有限探测。

5.2 多层重试

目标:验证不会出现 Gateway、Feign、HTTP Client、业务代码同时重试。

mermaid
flowchart TD
    A["原始1次请求"] --> B["Gateway重试2次"]
    B --> C["Feign重试3次"]
    C --> D["HTTP Client重试2次"]
    D --> E["最坏12次物理尝试"]

演练时要统计逻辑请求数和物理 attempt 数。如果物理 attempt 远高于预期,说明重试 Owner 没收敛。

5.3 写请求超时

目标:验证写请求状态未知时不会重复副作用。

流程:

mermaid
flowchart TD
    A["创建订单请求"] --> B["下游提交成功"]
    B --> C["响应被延迟或丢失"]
    C --> D["调用方得到超时"]
    D --> E["按幂等号查询事实"]
    E --> F["确认成功、失败或待处理"]

禁止演练中直接用新请求号再次提交。写请求故障演练必须提前准备幂等键、查询接口、补偿表和对账脚本。

5.4 注册中心不可用

目标:验证运行中调用方使用本地快照能短时间工作,但新实例发现和下线传播受影响。

要看:

  1. 已有调用是否继续成功。
  2. 新消费者启动是否失败。
  3. 新实例是否无法被发现。
  4. 旧实例摘除是否延迟。
  5. 本地快照年龄和 No instance 次数。

5.5 MQ堆积和恢复

目标:验证堆积时不会丢消息、不会无限重试、恢复后不会打垮下游。

要看:

  1. Lag 总量和 Lag 分布。
  2. 单分区或单队列是否热点。
  3. 消费重试和死信数量。
  4. 下游 DB/Redis/ES 是否成为瓶颈。
  5. 扩容消费者后是否受分区数限制。
  6. 恢复阶段是否限速,避免瞬时追赶打爆下游。

六、爆炸半径设计

演练必须从小到大。

阶段范围适合故障
本地/测试环境单机、Mock依赖基础错误分类、超时
预发环境接近生产链路注册中心、MQ、DB、Redis
生产单实例一个调用方或一个提供方实例连接池、熔断、下线
生产灰度租户指定租户或医院真实业务闭环
生产小比例流量1% 或 5%稳态验证
全链路灾备受审批控制机房切换、RPO/RTO

不能一上来全站断 Redis、断数据库、杀全部实例。那不是混沌工程,是制造事故。

七、观测证据

演练必须留下证据。

证据说明
Trace请求是否到达下游、attempt次数、目标实例
MetricsQPS、P95/P99、错误率、限流、熔断、线程池、连接池
Logs关键错误、降级原因、幂等号、traceId
业务事实订单、支付、库存、消息、ES、缓存是否一致
控制面注册中心、配置中心、规则版本是否生效
Runbook记录开始时间、停止时间、影响范围、恢复动作

没有证据的演练只能算“感觉没出事”。生产级演练要能回答:故障什么时候注入、影响了谁、保护器何时生效、业务事实是否正确、恢复用了多久。

八、JDK 8 Demo:演练停止条件

下面 Demo 模拟一个最小停止条件判断器。

java
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));
    }
}

输出:

text
healthyStop=false
dangerousStop=true

真实系统不能只靠人工盯屏。演练平台或脚本应该自动读取指标,触发停止条件后立即撤销故障规则并告警。

九、商业场景

9.1 医院接口变慢

假设:某医院接口从 300ms 变成 5s 时,只影响该医院采集任务,不拖垮其他医院。

验证:

  1. 该医院采集线程池是否满。
  2. 其他医院采集是否正常。
  3. 医院维度熔断是否打开。
  4. 失败任务是否进入可补偿状态。
  5. MQ Lag 是否只在该医院维度增长。

9.2 订单库存服务故障

假设:库存服务 500 或超时时,下单接口明确失败或返回待确认,不会伪成功,不会重复扣库存。

验证:

  1. 订单状态是否保持合法状态机。
  2. 幂等号重复提交是否复用结果。
  3. 重试次数是否符合预算。
  4. 库存服务恢复后 HALF_OPEN 是否平滑。

9.3 ES同步失败

假设:ES 写入失败不会影响 MySQL 主事务,失败事件进入死信或重试,后续能按 MySQL 重建。

验证:

  1. MySQL 事实源是否提交。
  2. Outbox/CDC 是否有事件。
  3. ES 消费失败是否可见。
  4. 重放后 ES 版本是否追平。

十、演练后复盘

演练结束不是“恢复了就完了”。要形成闭环:

输出说明
事实时间线故障注入、指标变化、保护生效、恢复时间
影响面租户、接口、实例、消息、数据
缺陷列表配置错误、监控缺失、Runbook缺失、代码问题
修复计划Owner、截止时间、验证方式
复演记录修复后同类故障再次验证
文档更新更新知识库、值班手册、面试素材

十一、面试标准回答

混沌工程不是随机断生产,而是在明确假设、稳态指标、爆炸半径、停止条件和回滚方案下做受控故障注入。比如验证库存服务延迟2秒时,订单服务是否按Deadline超时、Bulkhead是否限制线程占用、熔断是否按慢调用打开、降级是否符合业务语义、写请求是否按幂等号查询事实。演练范围要从测试、预发、生产单实例、灰度租户逐步扩大,观察Trace、P99、错误率、线程池、连接池、熔断状态、业务流水和消息状态。演练后要修复缺口并复演,而不是只证明“这次没挂”。

十二、本章小结

分布式系统的可靠性不是配置出来的,而是被验证出来的。超时、重试、熔断、限流、降级、幂等、补偿、对账、灾备,只有经过受控故障演练,才能证明在真实失败下能保护业务。