4. 常见的故障模式:网络延迟、丢包、磁盘 I/O 高、CPU 高负载、内存泄漏
故障演练搞了这么多年,我最常说的一句话就是:故障模式选对了,演练就成功了一半。很多人一上来就猛怼高并发、断网,结果系统瞬间崩了,连根因都找不到。其实混沌工程不是比谁下手狠,而是精准模拟最可能发生的故障。
这一章,我把平时扎扎实实用过的五种核心故障模式拆开讲——网络延迟、丢包、磁盘 I/O 高、CPU 高负载、内存泄漏。每个模式我都会告诉你怎么造、怎么观察、怎么收场。嗯,都是从真实项目里爬出来的经验。
4.1 网络延迟
先说说网络延迟。说白了,就是数据包在路上磨蹭了。可能是物理距离远,也可能是中间的路由器太忙。微服务架构里,一次跨服务调用多出几十毫秒延迟,整个接口就可能超时。
我在项目中遇到过一件事:线上某个支付服务,偶尔超时。查了三天,发现是数据库和缓存之间的网络链路偶尔抖动,延迟从 1ms 跳到 200ms。我们当时就给支付链路加了熔断 + 重试,后来再用延迟注入验证了一次,确认修复有效。
常见注入工具:tc qdisc(Linux)、toxiproxy(代理层)、chaosblade。
示例命令(给 eth0 增加 100ms 延迟):
tc qdisc add dev eth0 root netem delay 100ms
清理:tc qdisc del dev eth0 root netem
我的建议: 别一上来就加 1000ms。先从 10ms、50ms 起步,观察客户端超时设置和重试行为。很多系统在 200ms 以内还能扛,超过就开始雪崩。
4.2 网络丢包
丢包比延迟更致命。延迟只是慢,丢包直接让 TCP 重传、应用层连接断开。我曾经在压测环境里做过一次 5% 丢包注入,结果整个集群的 Thrift 调用全部失败,客户端大量重连,数据库连接池被打满。
为什么会这样?因为大多数框架默认的超时和重试策略都针对单次失败,没人考虑过“间歇性大规模丢包”的场景。你需要模拟的是 loss 1% ~ 5%,不是 100% 断网。
| 丢包率 | 典型影响 | 推荐演练场景 |
|---|---|---|
| 0.1% ~ 1% | 偶发超时,个别请求失败 | 验证重试机制是否触发 |
| 1% ~ 5% | 大量重传,连接池积压 | 验证熔断、降级、限流 |
| 5% ~ 10% | 连接超时,服务雪崩 | 验证是否具备自我保护 |
注意! 丢包注入一定要限制作用范围。我见过有人没加 dst 过滤,结果把整个机器的 DNS 请求也丢了,连控制台都访问不了。给个安全写法:
tc qdisc add dev eth0 root netem loss 3% 25%
25% 是相关性百分比,表示连续的丢包概率。生产环境建议先加 dst 参数过滤目标 IP。
4.3 磁盘 I/O 高
磁盘 I/O 高,我最早是在日志系统上栽的跟头。那次我们线上一个应用突然变慢,CPU 和内存都很正常,但磁盘的 iowait 飙到 60%。一查,是某业务团队开了一个全量数据导出任务,直接把磁盘 IOPS 吃满了。
磁盘 I/O 高的典型表现:iowait % 超过 30%,await 超过 100ms,应用接口整体变慢但 CPU 很低。你想想看,磁盘都卡住了,读写都得排队,业务能快才怪。
- 工具:
stress-ng、fio、dd、chaosblade - 常用参数:
stress-ng --hdd 4 --hdd-bytes 1G生成 4 个 I/O 进程,每个写 1G 数据 - 观察指标:
iostat -x 1看r/s、w/s、await、%util
个人经验: 不要只关注 %util。有些 SSD 性能很好,%util 到 100% 也不一定卡。真正要看的其实是 await 和 svctm 的比值。如果有条件,直接压测 IOPS 到硬件极限值的 80%,观察写日志、写缓存的阻塞情况。
4.4 CPU 高负载
CPU 高负载,大家都会模拟。但很多人搞混了两个概念:CPU 使用率高 和 CPU 负载高。使用率高是干活多,负载高是任务排队。我曾经见过一个 Java 应用 CPU 使用率才 20%,但 load average 到了 32——因为大量线程在等待锁。
混沌工程里的 CPU 故障,主要用来测试限流、降级、容器资源限制。如果你用的是 K8s,可以直接限制 Pod 的 CPU limit,观察 QoS 是否被驱逐。
开箱即用的注入方法:
# 使用 stress-ng 压满所有核心,持续 60 秒
stress-ng --cpu 0 --cpu-method matrixprod -t 60s
或者用 chaosblade 做更精细的熔断:blade create cpu fullload
我个人习惯先压到 70%~80%,看看业务线程有没有被饿死。很多框架的线程池空闲存活时间很短,一旦 CPU 高起来,线程数不够,新任务就全堵在队列里。
4.5 内存泄漏
内存泄漏,是我最怕的一种故障模式。因为它往往要跑很久才能发现。我记得在接手一个遗留服务时,每次上线一周后,OOM 就会准时出现。最后用 MAT 分析堆转储,发现是静态变量持有了一个不断增长的历史列表。
模拟内存泄漏,其实不需要写有 bug 的代码。直接用 stress-ng 的 --vm 参数就能占用内存:
stress-ng --vm 1 --vm-bytes 256M --vm-keep
--vm-keep 让进程持续占用内存不释放。你也可以用 chaosblade 的 blade create jvm memory 来精确控制堆内泄漏量。
踩过的大坑: 曾经一个团队用 stress-ng 压内存,结果把 swap 也用满了,整个宿主机不可用。记住:演练前先检查 swappiness,建议设为 0 或 10,避免内存压力通过 swap 扩散到其他进程。
4.6 五种故障模式对比
下面这张表格,是我每次做混沌工程培训时必讲的。它帮你快速判断该选哪种故障模式、关注哪些指标:
| 故障模式 | 影响范围 | 典型识别指标 | 推荐注入工具 |
|---|---|---|---|
| 网络延迟 | 服务间调用、API 响应 | P99 耗时、Timeout 异常 | tc, toxiproxy, chaosblade |
| 网络丢包 | 连接稳定性 | 重传率、连接失败率 | tc netem, chaosblade |
| 磁盘 I/O 高 | 日志、数据库、文件操作 | iowait, await, IOPS | stress-ng, fio, dd |
| CPU 高负载 | 计算密集型任务 | load average, 线程阻塞 | stress-ng, chaosblade |
| 内存泄漏 | 堆、元空间、物理内存 | GC 频率、OOM、swap 使用 | stress-ng, chaosblade jvm |
最后说一点心里话:上面五种故障模式,你不需要一次全用。我通常的做法是先做网络延迟 + 丢包,因为这是微服务最普遍的脆弱点;然后根据业务特点选一个存储或计算类的故障。演练不是比赛谁模拟的种类多,而是让团队在可控范围内提前体验真实的风险。你想想看,如果能在预发环境就发现异常,总比线上半夜被叫起来好,对吧?