系统与运维

磁盘 IO 瓶颈排查:从 iostat 到进程归因

"系统很卡"但 CPU 只用了 30%——瓶颈往往在磁盘队列。本文讲清 %util 与 %await 的本质区别、为什么 %util 100% 不等于坏了、iostat -x 各列的真实含义、PSI 压力指标如何反映「正在被拖慢」的进程,以及如何用 pidstat/fiotop 把等待时间归因到具体进程。

作者:巧匠团队·10 分钟阅读·更新于 2026-09-30

先分清:CPU 瓶颈还是 IO 瓶颈

一个快速判断:在 top 里看 load average 与 CPU 使用率的关系。如果 load 很高(比如 8)但 %Cpu(s) 里的 us+sy 远低于 100%(比如 40%),说明大量进程处于不可中断睡眠(D 状态),在等 IO。top 里 D 状态进程那一列是关键证据。

更直接的证据是 /proc/pressure/io。读 some avg10 这一行:如果值接近或超过 10,说明最近 10 秒内平均有 10% 的时间「至少有一个任务因等 IO 而被卡住」。这个数字比任何单一设备指标都更贴近你感受到的「卡」。

注意负载高也可能是 D 状态之外的伪影(比如大量僵尸进程、容器里 fork 风暴)。所以判断顺序是:先看 D 状态进程数量,再看 PSI,再看 iostat,三者一致才能下结论。

如果 PSI io 的 some 很低但 load 依然高,去看 load 的来源:ps -eo state,pid,comm | grep -c "^D" 精确计数,再 vmstat 1 看 b 列(阻塞在 IO 上的进程数)。若 b 长期大于 0 且 wa 列很高,说明确实是 IO 等待而不是 CPU 争抢。

ps -eo state,pid,comm | awk '$1=="D"' | head
grep -E "some|full" /proc/pressure/io
vmstat 1 5      # 关注 wa(iowait)与 b(阻塞数)

iostat -x:逐列读懂,别只盯着 %util

%util 只表示「采样窗口内设备非空闲时间占比」。对块设备它等价于「至少有一个请求在队列中的时间比例」,并不反映队列有多深。NVMe 这类并行设备可以 %util 100% 而吞吐与延迟完全正常——这是并行队列的正常表现,不是故障信号。

真正该看的是 await 与队列相关列。r_await / w_await(现代内核的 iostat -x 已拆分为读写分列)是平均请求延迟,包含排队时间。%aqu-sz(读请求平均排队时长)与 %arq-sz 分别表示读/写请求的平均队列深度,这两个指标升高意味着请求在排队等设备。

r/s w/s 是每秒请求数,rkB/s wkB/s 是吞吐。判断瓶颈形态看组合:await 高但 %arq-sz 低 → 设备本身慢(换盘或换存储类型);await 高且 %arq-sz 也高 → 队列拥塞,通常是并发太高或随机 IO 打满队列深度;await 低但 %util 高 → 大量小请求打爆并发度,是典型的「小文件风暴」。

采样间隔必须给对。iostat -x 1(间隔 1 秒)第一行是自开机以来的均值,没有参考价值——丢掉它,从第二行开始看。用 iostat -x 1 10 连看 10 次。若怀疑瞬时尖刺,用 iostat -x 0.5 20 提高时间分辨率。

iostat -x 1 10
# 关注列:r_await/w_await(平均延迟,含排队)
#        %arq-sz / %aqu-sz(请求平均队列深度 / 排队时长)
#        rkB/s、wkB/s(吞吐)、%util(只是非空占比)

PSI:把「设备慢」翻译成「谁被拖慢」

PSI(Pressure Stall Information)统计的是「任务因某类资源不足而失去运行机会」的时间占比。它是唯一能把内核视角的等待映射到「有多少任务受影响」的指标,而且开销极低,适合长驻采集。

/proc/pressure/io 有两行:some 统计至少一个任务因 IO 被卡的时间比例,full 统计「所有非空闲任务都因 IO 被卡」的时间比例——后者意味着系统已经彻底空转,全都在等盘。some 反映个体体验,full 反映系统级停摆,两者同时高就是灾难。

还应把 PSI 与 cgroup 结合。systemd 服务可以用 systemctl show <unit> -p IOReadPressureIOPressure 等属性读取该单元的 IO 压力,或直接读 cgroup 的 io.pressure 文件。这样能直接回答「是哪个服务把机器拖垮了」。

监控侧建议对 node_pressure_io_wait_seconds_total 做 rate() 再对时间归一化,得到 PSI 占用比例并设阈值告警(比如 > 40% 持续 5 分钟)。同时把 avg10/avg60/avg300 作为一组时间窗一起看,能区分短尖刺与持续劣化。

cat /proc/pressure/io
cat /sys/fs/cgroup/system.slice/<unit>/io.pressure
systemctl show nginx.service -p IOReadPressureIOPressure

按进程归因:谁在真正读写

iostat 告诉你「设备忙」,不告诉你「谁忙」。归因要用 pidstat -d 1,它按进程/线程统计 kB_rd、kB_wr、kB_ccwr(被取消的写请求——高值说明请求发出后被中断,浪费了带宽),iodelay 是该进程的 IO 延迟贡献。

pidstat 的输出是滚动的:每次刷新只显示本间隔内的值,所以要盯住 kB_wr 列的高值行,而不是看累计。用 pidstat -d 1 20 | sort 不现实(输出是流式的),实务中常见做法是连续观察几屏,记录下反复出现的 PID。

fiotop 适合交互式排查:它直接按 IO 速率排序列出进程,输出可读性好,比 pidstat 直观得多。缺点是默认依赖终端速度刷新,不适合脚本化,也不采历史。

如果 iotop 不可用,还有一个零依赖的办法:/proc/<pid>/io 暴露每个进程的 read_bytes / write_bytes 累计值(按实际落盘字节计,不含 page cache 命中)。间隔采样两次做差即可得到真实速率,适合在没有额外工具的最小化容器里应急。

pidstat -d 1
# 关注 kB_wr 高、kB_ccwr 也高的行:写风暴 + 请求被取消
cat /proc/$(pgrep -n nginx)/io | grep -E "read_bytes|write_bytes"
fiotop -o -d 1      # -o 按写入速率排序

根因分型与对应处置

小文件风暴(高 %util、低 await、高 %arq-sz):典型的 inode 与 fsync 密集负载,元数据操作打满队列。处置优先合并小文件为少量大文件,或改用支持直接 IO 的数据库/存储格式;短期可加内存页缓存,但要注意这只是在推迟 IO 而非消除。

大文件顺序吞吐(低 await、高 %arq-sz):通常是备份、同步、导出任务。核对业务是否必要,挪到业务低峰或限速(ionice -c3 空闲优先级、cgroup 的 blkio/io 权重)都能立竿见影。这类负载调优空间最大,收益也最直接。

高随机写(await 高且延迟分布长):多半是数据库 checkpoint 或日志刷盘。检查日志/备份配置是否超出性能预期,以及是否误把存储卷设成同步提交。网络存储(尤其 NFS)上尤其明显,此时把 /etc/fstab 里的同步挂载选项与存储侧缓存策略一起看。

最后别忽略内核参数这一层:/sys/block/*/queue/scheduler 确认调度器(none 适合高并发 NVMe,bfq 适合桌面/低并发),/sys/block/*/queue/nr_requests 控制请求队列深度,/sys/block/*/queue/rotational 判断是否机械盘。参数写进 /etc/sysctl.d/ 或 udev 规则才能重启后保持。

cat /sys/block/nvme0n1/queue/scheduler
# 高并发 NVMe 常用 none;低并发/桌面可试 bfq
cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/queue/rotational   # 0 = 非机械盘

官方参考来源

下方为命令对应的官方权威文档,供你核对最新用法与深入查阅。