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
常见故障模式分类与影响路径 故障注入 网络层 延迟 & 丢包 计算层 CPU 高负载 存储层 磁盘 I/O & 内存 超时 / 重试风暴 客户端阻塞 / 降级 OOM / 磁盘写满 验证故障隔离与容错策略

最后说一点心里话:上面五种故障模式,你不需要一次全用。我通常的做法是先做网络延迟 + 丢包,因为这是微服务最普遍的脆弱点;然后根据业务特点选一个存储或计算类的故障。演练不是比赛谁模拟的种类多,而是让团队在可控范围内提前体验真实的风险。你想想看,如果能在预发环境就发现异常,总比线上半夜被叫起来好,对吧?