磁盘 IO 被打满:一次写入阻塞导致接口雪崩的排查实录

磁盘 IO 被打满是生产环境里最容易被误判的故障之一:CPU 利用率不高、内存也充足,可接口就是越来越慢,最后整批超时。本期复盘一次真实事故——某个同步写日志的线程把磁盘 IO 打满,await 从 2ms 飙到 200ms,线程池被写阻塞占满,上游接口连锁雪崩。弄清”磁盘 IO 被打满”的链路与定位手法,能让你在下次告警里少绕三小时弯路。

一、现象:CPU 闲着,接口却垮了

最迷惑人的地方在于:监控里 CPU 才 30%、内存充足、网络也没打满,但 P99 延迟从 50ms 一路涨到 3s,最后大量接口超时。用 top 看不出明显热点,进程也没吃 CPU——这根本不是 CPU bound,而是典型的 IO bound。它和慢 SQL 拖垮 CPU 完全是两种病机:后者是计算先到顶,前者是磁盘先被写爆,而计算资源还在旁边干等。

判断第一枪很简单:看 load average 里 CPU 的 us/sy 都不高,但 wa(iowait)一栏却很高,甚至超过 20%——基本就可以锁定”磁盘 IO 被打满”方向,往下用工具取证即可。

二、定位:iostat 一眼看穿 await 与 %util

首选 iostat -x 1 看扩展统计。持续盯住目标盘(如 sdb),关键看两列:

# 每 1 秒刷新一次扩展统计,只看磁盘
iostat -x 1
# 也可指定设备与采样次数
iostat -x sdb 1 5

核心指标含义如下,记住”饱和”看 %util、”排队”看 await:

指标含义告警信号
%util设备繁忙时间占比长期 >85% 即接近饱和
await单 IO 平均等待时间(队列+设备)远超基线(>50ms)即在排队
r_await / w_await读/写各自的等待写 await 暴涨常是日志或落盘
aqu-sz平均排队深度持续 >1 说明请求在堆积

经验法则:%util 长期 100% + await 远大于设备正常服务时间,就是”磁盘 IO 被打满”的铁证。具体取证手法可参考Linux 性能排查实战里 iostat 与火焰图那节,这里聚焦磁盘维度。

三、追凶:到底哪个进程在狂写

磁盘饱和了,得抓住”凶手进程”。三板斧按从粗到细:

# 1) 只看有实际 IO 的进程,按写排序
iotop -o -P

# 2) 按进程统计磁盘读写(KB/s),定位 PID
pidstat -d 1

# 3) 看某进程打开了哪些文件(REG=普通文件)
lsof -p 1234 | grep REG

# 4) 反向查:谁在占用某个挂载点
fuser -v /data

iotop 的 -o 只显示有 IO 的进程,瞬间就能看到是哪个 PID 在疯狂写;pidstat -d 给出每个进程的读写速率,方便比对”谁贡献了大部分带宽”;剩下两步是用来确认它到底在写哪个文件/目录,为后续止血指明靶点。

四、根因归类:五类把磁盘写爆的姿势

抓到进程后,根因通常逃不出下面五类。对号入座能省掉大量瞎试时间:

根因典型信号对应手段
日志无轮转 / 级别过低单日志文件几十 GB,debug 全开logrotate + 调高日志级别
大事务 / 慢 SQL 落盘binlog、临时表猛写结合MySQL 调优慢 SQL 排查
应用同步写日志业务线程混着做写盘异步 appender、写读线程池隔离
备份 / rsync 与高峰重叠定时任务撞上流量高峰错峰、限速、限带宽
文件/临时文件泄漏小文件海量堆积参考fd 泄漏排查清理

五、止血与根治

止血(立刻降影响):把狂写进程降到最低 IO 优先级,先让业务喘口气;用 systemd 直接给服务限带宽;临时停掉写入方或加挂载选项缓解。

# 把 PID 的 IO 调度降到 idle(仅在其他IO空闲时才写)
ionice -c 3 -p 1234

# systemd 服务里直接限制(写入 /etc/systemd/system/xxx.service 的 [Service])
# IOAccounting=yes
# IOWeight=10
# IOReadBandwidthMax=/data 10M
# IOWriteBandwidthMax=/data 20M

# 临时挂载选项缓解(减少对元数据的写入)
mount -o remount,noatime,nodiratime /data

根治(不再复发):① 日志上 logrotate 切割 + 异步 appender,绝不在业务线程里同步 flush;② 写操作异步化、批量落盘,用消息队列削峰;③ 容量上换 SSD、NVMe 用 none/mq-deadline 调度器,挂载加 noatime;④ 大事务拆小、慢 SQL 先治本。内核层可调参数可参考Linux 内核参数调优里的 vm/dirty 相关项(如 vm.dirty_ratio、vm.dirty_background_ratio),让脏页回写更平滑。

六、本次事故复盘(真实链路)

把链路串起来就是:应用同步写访问日志 → 磁盘 await 飙到 200ms → 写线程被阻塞 → 业务线程池与写共用同一批线程(或异步写共用队列)→ 可用线程耗尽 → 接口超时 → 上游重试放大流量 → 雪崩。它和Java 内存泄漏fd 泄漏同属”资源被悄悄吃光”家族,但磁盘 IO 更隐蔽:它不 OOM、不抛异常,只是慢,所以常常被当成”网络抖动”误判。

三条关键教训:① 日志必须异步 + 限流,不能进业务关键路径;② 写盘操作和对外接口要线程池隔离;③ 监控要盯 await / %util,而不是只看磁盘”空间使用率”——空间满会报错,IO 饱和却只变慢。

七、把磁盘 IO 纳入监控与预防

与其等雪崩,不如把”磁盘 IO 被打满”前置成告警。node_exporter 已经暴露 node_disk_io_* 系列指标,配 Prometheus + Grafana 画面板即可;压测阶段就用 fio 摸清楚磁盘上限,避免上线才暴雷。

# 先用 fio 摸清磁盘顺序写上限(上线前必做)
fio --name=seqwrite --rw=write --bs=1M --size=2G \
    --numjobs=1 --runtime=60 --time_based --iodepth=1

# Prometheus 告警规则:磁盘持续饱和
- alert: DiskIOSaturated
  expr: rate(node_disk_io_time_seconds_total[5m]) > 0.85
  for: 5m
  labels: { severity: warning }
  annotations:
    summary: "磁盘 IO 使用率持续超过 85%"

告警阈值建议:%util 连续 5 分钟 >85%,或 await 持续 >50ms 即触发。云厂商监控一般直接提供”磁盘 IO 使用率 / IO 等待”面板,没有自建 Prometheus 也能很快接上。更多性能定位套路见Linux 性能排查实战

八、小结

磁盘 IO 被打满“的排查三板斧:iostat 看饱和(%util/await)、iotop/pidstat 抓凶手进程、logrotate/异步化/SSD 治根本。它最阴险的地方是”不报错只变慢”,所以监控必须前置到 await 与 %util,而不是只看空间。需要提醒的是,云硬盘(如阿里云 ESSD)的 IO 性能常与容量档位挂钩,突发额度耗尽后会跌回基线,这类”被限速”同样表现为 await 暴涨——排查时记得先翻云监控里的”被限制 IO”计数,别只盯本地指标。今天就能动手的两步:给服务器加一条磁盘 await 告警,再把你家日志的 appender 改成异步——两步就能挡掉八成这类雪崩。

上一篇 MCP 模型上下文协议实战:用统一协议连接大模型与工具
下一篇 fzf 模糊查找实战:重塑你的终端工作流