故障注入理论基础:故障模型与注入层次
做混沌工程这几年,我最大的体会是:故障注入不是乱搞。你想想看,如果连故障类型都分不清楚,就往生产环境里扔炸弹,那跟闭着眼睛拆弹有什么区别?
这一章,咱们把故障模型和注入层次彻底聊透。说白了,就是搞清楚两件事:能注入什么故障,以及在哪个层面注入。
故障模型分类:崩溃、延迟、错误、篡改
我习惯把故障模型分成四大类。这四类几乎覆盖了线上所有你能遇到的异常场景。
| 故障类型 | 核心表现 | 典型场景 |
|---|---|---|
| 崩溃(Crash) | 进程/服务/节点突然停止响应 | OOM、段错误、硬件宕机 |
| 延迟(Latency) | 响应时间显著增加,但未完全中断 | 网络抖动、磁盘 I/O 瓶颈、GC 停顿 |
| 错误(Error) | 返回异常状态码或错误数据 | HTTP 500、数据库连接超时、认证失败 |
| 篡改(Tamper) | 数据或配置被非预期修改 | 缓存污染、配置中心误操作、中间人攻击 |
我的经验之谈: 很多团队只关注「崩溃」和「延迟」,觉得服务没挂、响应没慢就万事大吉。但我在项目中遇到过好几次「错误」和「篡改」引发的连锁故障——比如某个配置项被意外篡改,导致整个集群的负载均衡策略失效,最终引发雪崩。所以,四类故障都要覆盖,一个都不能少。
1. 崩溃(Crash)
崩溃是最直观的故障。进程没了,端口不通了,心跳断了。嗯,这没什么好说的。
但要注意:崩溃不一定是瞬间的。我曾经遇到过 Java 进程 OOM 后,JVM 还在缓慢地做最后的 GC 尝试,端口虽然还开着,但已经无法处理任何请求。这种「半死不活」的状态,比直接崩溃更难排查。
注入建议: 用 kill -9 模拟硬崩溃,用 kill -15 模拟优雅关闭。两者表现完全不同,你的系统应该都能应对。
2. 延迟(Latency)
延迟是混沌工程里最「阴险」的故障。为什么?因为系统不会直接挂掉,但用户体验会急剧下降,而且很难快速定位根因。
我个人习惯把延迟分为两类:
- 固定延迟: 每次请求都增加固定的等待时间,比如 500ms。适合测试超时机制。
- 抖动延迟: 延迟时间随机变化,比如 100ms~2s 之间波动。更贴近真实网络环境。
我记得有一次线上事故,就是某个中间件偶尔出现 200ms 的抖动延迟。监控上看平均延迟只涨了 30ms,但 P99 延迟直接飙到了 3 秒。如果只盯着平均值,你永远发现不了问题。
3. 错误(Error)
错误故障的核心是:服务还在,但返回了错误。比如 HTTP 503、数据库查询超时、RPC 调用返回异常码。
这里有个坑——错误码的语义。我曾经在项目中遇到一个情况:我们注入了一个「数据库连接池耗尽」的错误,但客户端代码把所有的异常都当成「可重试」来处理,结果导致重试风暴,直接把数据库打挂了。所以,注入错误时,一定要搞清楚你的客户端是怎么处理这些错误码的。
避坑指南: 不要只注入 500 错误。试试 429(限流)、503(服务不可用)、504(网关超时)。不同错误码的客户端行为差异很大。
4. 篡改(Tamper)
篡改是最容易被忽视的故障类型。它不直接破坏服务,而是悄悄地改变数据或配置,让系统在「看起来正常」的状态下做出错误决策。
常见的篡改场景:
- 缓存中的 key 被篡改,导致返回错误数据
- 配置中心的某个开关被意外翻转
- 数据库中的关键字段被修改
- 请求头中的认证信息被篡改
嗯,篡改故障的排查难度是最高的。因为监控通常只检查「服务是否存活」「响应是否正常」,但不会检查「数据是否正确」。我建议在注入篡改故障时,一定要配合数据校验的监控,否则你根本不知道故障已经发生了。
故障注入的层次:基础设施、平台、应用
聊完故障模型,咱们再来看注入层次。我习惯把系统分成三层:基础设施层、平台层、应用层。每一层能注入的故障类型和影响范围都不一样。
1. 基础设施层
基础设施层是最底层,也是最「暴力」的注入层次。说白了,就是直接对物理或虚拟资源动手。
常见的注入手段:
- 节点宕机: 直接 shutdown 一台服务器
- 网络故障: 丢包、延迟、分区(network partition)
- 磁盘故障: IO 限速、磁盘写满、磁盘只读
- DNS 故障: 域名解析失败或返回错误 IP
我在项目中遇到过最经典的基础设施故障是「磁盘写满」。当时监控显示磁盘使用率 85%,看起来还好。但实际上某个日志文件被删除了但进程还持有文件句柄,导致磁盘空间没有真正释放。嗯,这种故障,光靠基础设施层的监控是不够的,还得配合平台层的文件系统检查。
注入建议: 基础设施层的故障影响范围最大,建议先从「非核心节点」开始注入。别一上来就把主库所在的机器给 shutdown 了。
2. 平台层
平台层指的是中间件和容器编排层。这一层的故障,往往比基础设施层更「隐蔽」,但影响面同样不小。
常见的注入场景:
- 数据库: 连接池耗尽、慢查询、主从切换延迟
- 缓存: Redis 宕机、缓存穿透、缓存雪崩
- 消息队列: Topic 分区不可用、消费积压、消息丢失
- 服务网格: Sidecar 注入延迟、Envoy 配置错误
我个人觉得,平台层的故障注入是最「划算」的。为什么?因为这一层的故障既能暴露应用代码的缺陷,又不会像基础设施层那样容易引发大规模不可用。你想想看,如果 Redis 挂了,你的应用有没有降级方案?如果数据库主库切换了,你的连接池会不会瞬间爆掉?这些都是平台层故障能帮你验证的。
3. 应用层
应用层离业务代码最近。这一层的故障注入,说白了就是在代码层面制造异常。
常见的注入方式:
- 方法延迟: 在某个关键方法中增加 sleep
- 返回错误: 强制让某个接口返回 500 或自定义错误码
- 参数篡改: 修改请求中的关键参数
- 抛出异常: 在特定条件下抛出运行时异常
我记得有一次,我们在一个支付服务中注入了「订单状态篡改」的故障。结果发现,下游的对账系统完全没有做数据校验,直接把篡改后的状态写入了财务数据库。嗯,这个故障要是发生在生产环境,后果不堪设想。
避坑指南: 应用层的故障注入要特别注意「副作用」。比如你篡改了一个订单金额,这个数据可能会流入多个下游系统。注入前一定要想清楚:这个故障会不会污染真实数据? 如果会,记得注入完成后做数据清理。
故障模型 × 注入层次:组合矩阵
把故障模型和注入层次组合起来,就形成了一个 4×3 的矩阵。我建议你在设计混沌实验时,至少覆盖矩阵中的 70% 的组合。
| 故障模型 \ 注入层次 | 基础设施层 | 平台层 | 应用层 |
|---|---|---|---|
| 崩溃 | 节点宕机 | Redis 主节点宕机 | 进程 OOM |
| 延迟 | 网络延迟 100ms | 数据库连接池等待 | 方法内 sleep(500) |
| 错误 | DNS 解析失败 | MySQL 连接超时 | 接口返回 503 |
| 篡改 | iptables 规则篡改 | 配置中心 Key 被改 | 请求参数被篡改 |
我的建议: 不要一次性覆盖所有组合。先从「应用层 × 错误」开始,因为这一组合的影响范围最小,排查也最容易。等团队积累了经验,再逐步扩展到平台层和基础设施层。