Linux 磁盘 IO 监控实战:6 症状决策树 + iostat/iotop 判读 + 5 个真实事故
我干运维第十个年头,遇到最多的"疑难杂症"不是 CPU 满、不是内存泄漏,而是磁盘 IO。
原因很反直觉:磁盘出问题,最先报警的往往不是磁盘监控,而是 load average 飙到几十、业务接口超时、数据库连接池打满。你盯着 CPU 看,%us 才 5%,%idle 还有 60%——CPU 一点都不忙,可系统就是卡得要死。这种"load 高 CPU 闲"的经典现场,我第一年就踩过,还以为是机器中毒了,重装了两遍系统。
后来才明白:load 统计的是可运行 + 不可中断睡眠的进程数。当进程卡在磁盘 I/O 上进入 D 状态(不可中断睡眠),它照样计入 load,但 CPU 是空的。这就是"load 高 CPU 闲"的真相——八成是 iowait(IO wait)在作怪,不是 CPU 满,不是病毒。
这篇我把自己十年的 IO 排障经验完整写出来:从 vmstat 60 秒定性,到 iostat -x 逐字段判读,到 iotop/pidstat 揪出元凶进程,再到 blktrace 下探块设备层,最后是 cgroup v2 的 blkio 限流。全文给出一张 6 症状决策树,一张 HDD/SSD/NVMe 阈值矩阵,外加 5 个我亲手处理过的真实事故。所有命令都可直接复制,阈值都写到具体数字。
这篇适合谁
- 运维/后端/数据库工程师:需要自己动手排 IO 瓶颈的
- SRE/平台工程师:容器场景被邻居进程拖垮 IO 的
- 刚入行的同学:想建立"IO 瓶颈 vs CPU 瓶颈"正确判断的
工具依赖速查
本文用到的主工具与安装方式(各发行版):
# RHEL/CentOS/Rocky
yum install -y sysstat iotop blktrace
# Debian/Ubuntu
apt install -y sysstat iotop blktrace
# 检查是否已装
which iostat iotop pidstat blktrace
注意 sysstat 包提供 iostat、pidstat、sar、mpstat 四件套,装一次全有。iotop 和 blktrace 是独立包,别漏装。
一、为什么 IO 瓶颈是运维最易误判的问题
先讲清楚"load 高 CPU 闲"这个经典陷阱的成因,后面所有排查都从这出发。
Linux 的 load average 统计的是运行队列里可运行进程 + 不可中断(D 状态)睡眠进程的数量。关键就在"不可中断睡眠"这五个字:
- 进程等 CPU 时,状态是 R(running),计入 load,CPU 也在忙——正常对应关系
- 进程等磁盘 I/O 时,状态是 D(uninterruptible sleep),照样计入 load,但 CPU 是空的
所以你看到 load 20、CPU 5%,不是 CPU 不够,是有 20 个进程在排队等磁盘。我把它叫"影子瓶颈":磁盘是那堵墙,CPU 是无辜的旁观者。
最常见的 5 个误判方向
| 误判 | 错误操作 | 真实原因 |
|---|---|---|
| 以为是 CPU 满 | 加 CPU、扩核 | 是 iowait,磁盘慢 |
| 以为是病毒/挖矿 | 杀进程、重装系统 | D 状态进程在等 IO |
| 以为是内存泄漏 | 加大内存 | 页缓存回写(flush)压盘 |
| 以为是应用 bug | 重启应用 | 是其他进程抢 IO |
| 以为是网卡 | 查网络抓包 | 是磁盘随机读风暴 |
这 5 个误判我都犯过至少一次。排障第一原则:先分清是 CPU、内存还是 IO,再往下查。 分清之后,IO 内部的排查其实非常模式化。
load 的三个组成部分,别被平均数骗
load average 是"1 分钟 / 5 分钟 / 15 分钟"三个数,很多人只盯它。但它把三种完全不同的事混在一起:
| 计入 load 的进程 | 状态 | 对应问题 |
|---|---|---|
| 可运行进程(等 CPU) | R | CPU 不足 |
| 不可中断睡眠进程(等 IO) | D | 磁盘 IO 瓶颈 |
| 不可中断睡眠进程(等锁/驱动) | D | 内核死锁、盘挂、驱动 bug |
所以load 高本身没意义,要看"组成 load 的是 R 还是 D"。判断方法:
R=4 D=18:load 主要是 18 个 D 状态进程在等 IO → IO 瓶颈(本文主题)R=40 D=0:load 全是可运行进程在等 CPU → CPU 瓶颈,去看 cpu-monitorR=4 D=18但iostat %util不高 → 可能是 NFS/多队列,见症状 5
记住:load 是"症状",R/D 分布才是"病因"。看到 load 高,先别慌,花 3 秒跑上面这条命令分清是 R 还是 D,方向就定了。
为什么"加 CPU"救不了 IO 瓶颈
这是运维最常花冤枉钱的地方。IO 瓶颈的本质是进程在等磁盘,不是等 CPU。你加了 16 个核,进程照样在等那根磁头转过来、等那块闪存写完。CPU 增加不改变磁盘的物理速度,唯一的变化是 load 数字看起来"更合理"了(因为可运行进程少了),但业务延迟纹丝不动。
正确的心智模型:磁盘是水管,CPU 是水龙头。水管细了,你开再多的水龙头(加 CPU),水龙头也不出水(进程照样等)。该换粗水管(换盘),不是多开水龙头。
一个判断"是不是 IO 瓶颈"的快检
不想分析太细,先跑一条命令做初筛:
wa > 20% 基本就锁定 IO 了。wa(iowait)是"CPU 空闲但等着 IO 完成"的时间比,它高说明 CPU 在给磁盘"干等"。看到 wa 43%,别犹豫,进第二章 vmstat。
6 症状决策树总览
一张图记住 IO 排障的 6 条路径(对应 6 种典型症状):
症状 1:load 高,CPU 闲(%wa > 20%)
└─→ iowait 真凶 → 查 iostat 设备级 → 定位盘
症状 2:应用卡,但 iostat %util 低 / 100% 但不慢
└─→ NVMe 假饱和 / 队列合并 / NFS → 看 rMB/s + await
症状 3:D 状态进程堆积(ps 里一堆 D)
└─→ 内核不可中断睡眠 → wchan 定位函数 → 盘坏/驱动
症状 4:随机小 IO vs 顺序大 IO
└─→ r/s 高而 rMB/s 低 = 随机读 → HDD 灾难
症状 5:单一进程 vs 全局(iowait 高但 iostat %util 低)
└─→ 单进程抢 IO → iotop/pidstat 揪元凶
症状 6:缓存回写压垮盘(flush/jbd2 占顶)
└─→ 内存脏页回写风暴 → vm.dirty_ratio 调优
下面六章就顺着这 6 条路径逐个展开,每章都是"症状 → 命令 → 判读 → 决策"。
二、60 秒判断:vmstat 1 的 r/b/wa 三列
IO 排障第一步永远是 vmstat 1,60 秒就能定性。它比 top 更干净,直接给你"系统是卡在 CPU 还是卡在 IO"的答案。
一次典型的生产事故输出(这是当年那台"重装两次"的机器):
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
3 14 2048 102356 110024 889204 0 0 0 112 102 180 2 1 55 42 0
4 15 2048 102180 110024 889240 0 0 0 2080 105 190 2 1 54 43 0
5 16 2048 101876 110024 889312 0 0 0 4152 108 195 1 1 53 45 0
2 14 2048 101900 110024 889260 0 0 0 880 104 182 2 1 56 41 0
怎么读这三列:
| 列 | 含义 | 判读 |
|---|---|---|
| r | 可运行进程数(等 CPU) | 持续 > 核数 → CPU 不足 |
| b | 不可中断睡眠进程数(等 IO) | 持续 > 1 就是 IO 瓶颈的强信号 |
| wa | CPU 等待 IO 完成的时间百分比 | > 20% 即 IO 有问题,> 50% 是瓶颈 |
上面这份输出:b 稳定在 14-16(有十多个进程在等 IO),wa 稳定在 41-45%,us 只有 2%,idle 还有 53%。CPU 明明闲着一半,却有十几个进程在排队等磁盘——这就是"load 高 CPU 闲"的铁证。 方向确定:磁盘 IO,不是 CPU。
两个补充信号:swap 与 bi/bo
si/so持续非零 → 内存在换页,这是"swap 抖动"的源头,会跟 IO 互相踩踏(见事故二)bo飙高 → 大量写回(可能脏页回写或 sync),结合wa高一起看bi高但bo正常 → 读密集,多为随机读,重点查iostat的r/s与r_await
决策:vmstat 只能定性"是 IO 问题",不能定位"是哪块盘、哪个进程"。定性后立刻进第三章 iostat,别在 vmstat 上恋战。
再补三个易被忽略的字段
vmstat 还有几列能帮你缩小范围:
| 列 | 含义 | 判读 |
|---|---|---|
| cs | 上下文切换次数/秒 | 飙高但 r 不高 → 进程频繁让出 CPU,多为锁等待或 IO 等待 |
| in | 中断次数/秒 | 飙高 → 硬件中断风暴,配合 si 看是否软中断 |
| bi / bo | 块读/写 /秒 | bi 高→读密集,bo 高→写密集,结合 wa 分主次 |
举个区分例子:如果 cs(上下文切换)很高而 r 不高、wa 也不高,那大概率不是 IO 也不是 CPU,而是锁竞争(比如数据库行锁、JVM 锁)。cs 是排除法里的一把好刀:排除掉 CPU 和 IO 之后,cs 高基本就是锁或调度问题。
vmstat 与 top 的配合
vmstat 看的是系统整体,top 看的是 CPU 细分。两者结合才能分清"是磁盘慢,还是应用在等别的东西":
注意 wa 43.0 和 id 53.4:CPU 一半在等 IO,一半真的空闲。us + sy 只有 3.4%,应用几乎没在烧 CPU。 这说明瓶颈在磁盘等待,应用是"被 IO 卡住"而不是"跑不动"。
一个反直觉细节:wa 高不代表 CPU 在忙,恰恰相反,wa 高说明 CPU 闲着没事干。所以看到 wa 43% 去扩 CPU 是完全错误的方向——你是在给一个已经闲着的组件加资源。
三、系统级:iostat -x 1 逐字段判读
iostat -x 1 是 IO 判读的核心工具。-x 展开所有字段,1 每秒采样。重点不是看那一串数字,而是看趋势和相对关系。
先看设备级概览(生产库节点,机械盘):
Device r/s rkB/s rrqm/s %rrqm r_await w/s wkB/s wrqm/s %wrqm w_await aqu-sz rareq-sz wareq-sz await %util
sda 12.00 128.00 0.00 0.00 180.00 2.00 64.00 0.00 0.00 95.00 12.30 10.67 32.00 165.00 100.00
sdb 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
逐字段判读表
| 字段 | 全称 | 判读标准 | 关键点 |
|---|---|---|---|
| %util | 设备忙时比 | > 80% 开始警惕,100% 也不一定饱和 | NVMe 要警惕假 100%(见事故四) |
| await | 平均 IO 等待 | HDD > 20ms 慢,> 200ms 爆 | 分盘种,见阈值矩阵 |
| r_await | 读等待 | 读延迟,随机读场景最重要 | 单独看,别被 w_await 平均掉 |
| w_await | 写等待 | 写延迟,fsync 敏感场景 | 和 r_await 一起看 |
| aqu-sz | 平均队列长度 | > 设备并发能力即排队 | 高 await + 高 aqu-sz = 队列堵死 |
| rrqm/s | 读请求合并率 | 高 = 顺序读被合并 | 合并率高说明顺序 IO,低说明随机 |
| rareq-sz | 平均读请求大小 | > 64K 顺序,< 16K 随机 | 判断随机 vs 顺序的核心 |
| rkB/s | 读吞吐 | 结合 rareq-sz 判断类型 | 看带宽是否到设备上限 |
判读三步法
第一步:看 rareq-sz / wareq-sz 判断 IO 类型。
rareq-sz只有 8-16K(上面输出是 10.67K)→ 随机小 IO。对机械盘这是灭顶之灾,因为每次都要寻道rareq-sz> 64K → 顺序大 IO,能发挥盘的全部带宽,不用太担心- 上面那份输出
rareq-sz 10.67+r_await 180ms+%util 100%:随机读把机械盘彻底打穿了,每次读都要寻道,延迟 180ms。这是数据库随机 IO 压垮机械盘的典型画面(完整复盘见事故三)
第二步:看 await 和 %util 的相对关系。
%util 100%+await高 → 设备真的忙,队列在排队,是瓶颈%util 100%+await低(NVMe 常见)→ 假饱和,设备其实没到极限,业务慢另有原因%util低 +await高 → 单盘多队列或 NFS,设备没满但请求等得久(对应症状 5)
第三步:看 aqu-sz(队列深度)。
- 高
aqu-sz+ 高await= 队列在堆积,请求排长队,这是瓶颈的铁证 - 高
aqu-sz+ 低await= 设备并发能力强(NVMe),排队但消化快,未必是瓶颈
一个必背的组合:r_await 高但 w_await 低 → 读是问题;w_await 高但 r_await 低 → 写是问题(多为脏页回写或 fsync,见事故五);两者都高 → 全面饱和。
iostat 判读 4 个实战细节
细节 1:-x 的 %util 是"忙时比",不是"利用率"。
%util = 设备忙的时间 / 总时间。一个请求都没排队时,%util 也可能接近 100%,因为它在持续服务请求。判瓶颈要看 await 是否同步高,%util 单独看会骗人(尤其 NVMe)。
细节 2:aqu-sz(平均队列长度)要跟设备的"并发能力"比。
机械盘并发能力约 1(一个磁头一次一个请求),aqu-sz > 2 就开始排队;NVMe 并发能力几百上千,aqu-sz 50 也不算堵。队列长度是相对的,必须分盘种。
细节 3:rrqm/s 和 wrqm/s 是"合并率"不是"请求数"。
合并率高说明块层把相邻请求并成一个了,是顺序 IO 的特征。如果 rrqm/s 很低但 r/s 很高,说明请求彼此不挨着——随机 IO,机械盘灾难。%rrqm = rrqm / (rrqm + r) 超过 80% 基本是顺序流。
细节 4:avgrq-sz(在旧版 iostat 里叫 avgqu-sz 旁边那列)是平均请求大小。
avgrq-sz 单位是扇区(512B),比如 avgrq-sz 20 就是 10KB。小于 32(16KB)基本是随机小 IO,大于 128(64KB)是顺序大 IO。这个字段配合 rrqm 是判断"随机 vs 顺序"最快的一对组合。
iostat 读数的"采样间隔"陷阱
iostat -x 1 是每秒一次,但 iostat 的字段是两次采样之间取平均。第一次输出往往是自启动以来的累计平均,不准,要跳过第一行。正确姿势:
只看第一次的输出是新手最常犯的错:第一次是"开机到现在的平均",可能把 3 天前的 IO 峰混进来,await 被拉高,误导判断。看第 2 秒及以后,才是"当前真实状态"。
多盘与分区:先 iostat -x 看全部,再 iostat -xm 1 深挖
机器上可能挂了很多盘。先不加参数看全貌,快速锁定"哪块盘异常":
grep 把 loop/ram 这类虚拟盘过滤掉,只看真盘。锁定问题盘后,再用 -m(单位切到 MB)和 -p(带分区)看细节:
分区级视角很重要:有时候整盘 %util 100%,但深挖发现是 sda1(日志分区)在写,sda2(数据分区)其实还好。分盘不彻底时,分区级能帮你把"谁拖累谁"看清楚。
四、进程级:iotop -oPa 与 pidstat -d 1
iostat 告诉你"哪块盘出问题",但"哪个进程在作恶"要靠 iotop 和 pidstat。先 iostat 定盘,再 iotop 定进程,顺序别反。
iotop 的两个关键参数
-o:只显示正在执行 IO 的进程(otherwise 的进程会淹没你)-P:按进程而非线程聚合(想细看线程去掉 -P)-a:显示累计 IO,而不是每秒变化——累计值更稳定,不会一闪而过
典型输出(生产事故,定位到元凶):
Total DISK READ : 48.60 M/s | Total DISK WRITE : 2.34 M/s
Actual DISK READ: 48.60 M/s | Actual DISK WRITE: 2.30 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND
1234 be/4 backup 48.56 M/s 0.00 B/s 0.00 % 99.99 % rsync -a --delete /data/ /backup/
987 be/4 postgres 0.00 B/s 2.28 M/s 0.00 % 2.10 % postgres: walwriter
1 be/4 root 0.00 B/s 0.00 B/s 0.00 % 0.00 % systemd
一眼锁定:PID 1234 rsync 在读 48.56 M/s,IO 占 99.99%。这就是元凶。rsync -a --delete 没有限速,正在全量扫描 /data 并拷贝,把机械盘的读带宽吃干抹净(完整复盘见事故一)。
判读口径:IO> 列显示进程占用 IO 时间的百分比。如果某个进程 IO> 持续超过 30%,它基本就是 IO-bound 的元凶,可以定性为"这个进程在打盘"。要精确知道它占整个磁盘带宽多少,看 DISK READ/WRITE 与 Total 的比值。
pidstat -d 1 补充确认
iotop 需要 root 且依赖内核 IO 统计,个别环境会报 "Cannot execute" 或看不到数据,用 pidstat -d 兜底:
12:00:01 UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command
12:00:02 500 1234 49640.00 0.00 0.00 512 rsync
12:00:02 500 987 0.00 2334.00 0.00 3 postgres
kB_rd/s/kB_wr/s:进程的读写带宽iodelay:进程等待 IO 的毫秒数(这个字段经常被忽略,却是"进程在等盘"的直接证据)
上面 rsync 的 iodelay 512,说明它在 1 秒内平均等了 512ms 磁盘——这个进程超过一半时间在等 IO,是铁打的 IO-bound。对比 postgres 只有 3ms,完全正常。
决策:iotop 抓到元凶后,如果它不是核心业务,直接 ionice 降级或限速(见第十章);如果是核心业务,说明它是真的需要那么多 IO,得换盘或优化 SQL,别去压它。
iotop 每一列都在说什么
iotop 的列比很多人以为的更有用,逐列拆开:
| 列 | 含义 | 判读 |
|---|---|---|
| TID | 线程 ID | 加 -P 变进程 ID,线程场景去掉 -P |
| PRIO | IO 调度优先级 | be/4 是 best-effort 4,rt 是 realtime,idle 是空闲级 |
| DISK READ | 读带宽 | 元凶进程这里通常高得离谱 |
| DISK WRITE | 写带宽 | 写元凶(flush/jbd2)看这里 |
| SWAPIN | 换入占比 | 非零说明在从 swap 读,内存压力 |
| IO> | IO 时间占比 | > 30% 即 IO-bound,> 80% 基本是它在打盘 |
| COMMAND | 进程名 | 元凶的名字,一目了然 |
PRIO 列能帮你识破一个坑:如果一个进程显示 rt(realtime),说明它被 ionice -c1 设成了实时优先级,会抢占所有普通 IO,业务进程被它饿死。遇到这种情况,ionice -c0 -p <pid> 把它拉回 none,或者 ionice -c2 -n7 降级。
SWAPIN 列:非零说明进程在从 swap 读内存页,这是内存压力导致的额外 IO——不是进程自己要读盘,是内存不够在借盘当内存。看到 SWAPIN 高,先解决内存,别冤枉进程在打盘。
用 pidstat 区分"进程真的在打盘"还是"在等盘"
pidstat -d 的 kB_rd/s 大,说明进程发起了大量 IO;但有些进程 kB_rd/s 很小却 iodelay 很大,说明它是在等别人抢走的盘(症状 5 的典型)。这两者处理方式完全不同:
| 情况 | kB_rd/s | iodelay | 判断 | 处理 |
|---|---|---|---|---|
| 元凶进程 | 大 | 大 | 它在打盘 | ionice 限它 |
| 受害者进程 | 小 | 大 | 它被抢盘 | 处理元凶,别动它 |
| 正常进程 | 正常 | 小 | 无问题 | 不用管 |
iodelay 是"受害者识别器":一个 kB_rd/s 只有 2M 但 iodelay 300ms 的进程,它自己没打盘,却被别人打盘拖累。这时你去 ionice 它完全没用,要找那个 kB_rd/s 大的元凶。判"谁是元凶"看 kB_rd/s,判"谁在受害"看 iodelay。
iotop 的 3 个坑
- 必须 root:非 root 只能看自己的进程,
iotop会提示权限不足。生产用 sudo 或配 sudoers。 -o参数别省:不加-o会列出几百个"0 IO"的进程,真正的元凶被淹没。先-o缩小到"正在动 IO 的",再细看。-k或-K单位:默认动态单位(K/M/G),脚本化抓取用-k(固定 KB)或-b -n 1(批处理一次输出)更稳:
五、D 状态进程:ps 定位卡在内核函数
如果 vmstat 的 b 列持续很高、iowait 也高,但 iotop 里找不到明显元凶,那大概率是一堆进程卡在 D 状态。这是最让人头皮发麻的场景:进程杀不掉、重启失败、系统像僵尸一样半死不活。
state 列是 D 的进程就是不可中断睡眠。wchan 列显示它卡在哪个内核函数。
典型输出(机械盘损坏事故,多进程卡 D):
STATE PID PPID COMMAND WCHAN
D 2103 1 /usr/sbin/mysqld io_schedule
D 2118 1 /usr/sbin/mysqld blk_mq_submit_bio
D 2150 1 kworker/u8:1-flush-8:0 blk_mq_flush_plug_list
D 3011 2103 /usr/bin/rsync --bwlimit=0 io_schedule
D 4122 1 systemd-journald wb_workfn
wchan 判读:卡在哪个内核函数
| wchan | 含义 | 提示 |
|---|---|---|
io_schedule |
通用 IO 调度等待 | 盘在响应,只是慢,可等 |
blk_mq_submit_bio |
提交 bio 阻塞 | 块层排队,队列满 |
blk_mq_flush_plug_list |
刷 plug 列表阻塞 | 写回阻塞,脏页多 |
wb_workfn |
内核回写线程 | 脏页回写风暴(症状 6) |
jbd2_log_wait_commit |
ext4 日志提交等待 | ext4 日志翻倍(事故五) |
__wait_on_bit |
等锁位 | 多为文件系统锁,NFS 常见 |
关键区分:D 状态有两种结局。
- 盘活着,只是慢:进程在
io_schedule等真正的 IO,磁盘还在响应,等一会儿能完成 - 盘死了/掉盘/驱动崩:进程卡在 IO 层永远等不到完成,
kill -9无效(D 状态不可被信号打断),只能重启机器
怎么区分:看 D 状态进程是否在增长、iostat 是否还在更新(iostat 数值不动 = 盘已经没响应了)。如果 D 进程越积越多且 iostat 卡住不刷新,别犹豫,这是盘挂了,联系硬件厂商换盘,同时准备重启。
一个救急判断:D 状态进程 kill -9 后 ps 里还在,就是它。D 状态连 SIGKILL 都无视,普通 SIGTERM/SIGKILL 统统无效。这时候不要反复杀进程浪费时间,直接处理根因——要么等盘恢复,要么重启。
D 状态 vs 正常慢:别把"慢"误判成"死"
有个必须分清的边界:D 状态进程增多 ≠ 系统死了。 高并发下几十个进程同时等 IO,进入 D 状态是正常的排队现象,只要盘在响应就能恢复。区分"排队"与"死亡"看三点:
- 看 D 进程数是否在增长:稳定不涨 = 排队等盘,会消化;越积越多 = 盘跟不上,越来越糟
- 看
iostat数值是否还在刷新变化:还在变 = 盘活着;数值冻结 = 盘掉了 - 看
wchan是否集中在 IO 函数:io_schedule/blk_mq_submit_bio是 IO 排队;如果突然变成__wait_on_bit或mutex_lock,可能是文件系统或 NFS 层死锁,性质不同
D 状态进程的常见来源清单
D 状态不是"神秘现象",它几乎总来自以下几类,见到 D 先对号入座:
| 来源 | 典型进程 | 为什么进 D |
|---|---|---|
| 数据库随机读 | mysqld / postgres | 随机页读取,等寻道 |
| 备份/拷贝 | rsync / cp / tar | 读大量小文件,随机读 |
| 脏页回写 | flush-8:0 / wb_workfn | 内核回写线程被写堵 |
| 日志提交 | jbd2/sda-8 | ext4 日志提交等落盘 |
| swap 换页 | kswapd0 / kworker | 内存不足,换页等盘 |
| 网络文件系统 | nfs 相关 | NFS 后端盘慢,远端等待 |
定位技巧:看 wchan 列 + 进程名,基本能猜个八九不离十。比如 flush-8:0 是回写线程、jbd2/sda-8 是 ext4 日志——这两个出现,说明是"写"的问题不是"读"的问题。
用内核 hung task 机制兜底
如果 D 状态进程超过 120 秒(内核默认 hung_task_timeout_secs),内核会打印一次堆栈到 dmesg,这是免费的诊断信息:
# 查看当前阈值
cat /proc/sys/kernel/hung_task_timeout_secs
# 看内核有没有打印 hung task
dmesg -T | grep -i "hung task" | tail -20
[Thu Oct 29 02:14:35 2026] INFO: task mysqld:2103 blocked for more than 120 seconds.
[Thu Oct 29 02:14:35 2026] Not tainted 4.18.0-477.10.1.el8
[Thu Oct 29 02:14:35 2026] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[Thu Oct 29 02:14:35 2026] task:mysqld state:D stack: 0 pid: 2103 ppid: 1
[Thu Oct 29 02:14:35 2026] Call Trace:
[Thu Oct 29 02:14:35 2026] [<ffffffffb8a6f9c0>] __schedule+0x2b0/0x700
[Thu Oct 29 02:14:35 2026] [<ffffffffb8f7a5e0>] io_schedule+0x20/0x40
[Thu Oct 29 02:14:35 2026] [<ffffffffb8f89a10>] blk_mq_submit_bio+0x1b0/0x240
Call Trace 里 io_schedule + blk_mq_submit_bio 的组合,明确告诉你是块层 IO 排队,不是死锁。这一条 dmesg 胜过你猜半小时。
六、块设备层:blktrace 随机 vs 顺序、合并率
iostat 和 iotop 给的是聚合数据。当你想知道"IO 到底是不是随机、队列到底多深、请求有没有被合并",就要下探到块设备层——blktrace 是块层的 X 光片。
抓取与解析
# 抓 5 秒 sda 的块层事件,写入当前目录
blktrace -d /dev/sda -o /tmp/bt -w 5
# 解析并统计
blkparse /tmp/bt.sda -i /tmp/bt.sda.blktrace.0 -f "%D %2c %8s %5T.%9t %5p %2a %3d\n" > /tmp/bt_parse.txt
看解析后的每行:%T 时间、%p 进程 PID、%a action(如 Q 请求入队、D 设备提交、C 完成)、%d 设备号。
随机 vs 顺序的关键判读
看 action 序列和扇区号分布:
Q(入队)到C(完成)的时间差 = 单请求真实延迟- 连续多个
Q的扇区号相邻 → 顺序 IO - 连续多个
Q的扇区号跳跃很大 → 随机 IO
# 顺序 IO:扇区号连续递增
8,0 Q 12345 0.001234 2103 R 12000 + 32 [mysqld]
8,0 Q 12346 0.001240 2103 R 12032 + 32 [mysqld]
8,0 Q 12347 0.001246 2103 R 12064 + 32 [mysqld]
# 随机 IO:扇区号到处跳
8,0 Q 23456 0.002345 2103 R 987654 + 8 [mysqld]
8,0 Q 23457 0.002351 2103 R 12045 + 8 [mysqld]
8,0 Q 23458 0.002360 2103 R 777000 + 8 [mysqld]
第一种是顺序流(好),第二种是随机风暴(机械盘杀手)。扇区号跳跃 + 请求大小小(+8 = 4KB)+ 大量 Q,基本就是随机小 IO,也就是"数据库 B+ 树页读取"的典型形态。
合并率:看 %rrqm 和 blkparse 的 Q→D 比例
iostat 里 %rrqm(读请求合并率)高说明请求被块层合并了。合并的本质是把多个相邻扇区的请求合成一个大请求,对机械盘尤其重要——一次寻道服务更多数据。
%rrqm> 80% → 请求高度合并,实际是顺序 IO,不用担心%rrqm< 20% 且rareq-sz小 → 请求分散且小,随机读,机械盘大忌
实操建议:blktrace 信息量极大,别当常规工具,只在 iostat 判读有歧义、或要确认"随机 vs 顺序"、或做存储调优前后对比时用。日常用 iostat 的 rareq-sz + rrqm 两个字段就能完成 90% 的判断。
blkparse 的聚合统计(比翻日志高效)
blkparse 直接翻原始日志太累,用它的汇总统计一次看清全貌:
# -f 指定格式,-s 显示统计摘要,-i 指定输入
blkparse -i /tmp/bt.sda.blktrace.0 -f "%D %2c %8s %5T.%9t %5p %2a %3d\n" -s
Total: 123456 (reads: 98000, writes: 25456)
Merged: 0 (reads: 0, writes: 0)
Queue: 123456, Complete: 123456
Read: 98000 reqs, 480000 KB (4.9 KB/req) ← 平均请求 4.9KB = 随机小读
Write: 25456 reqs, 320000 KB (12.5 KB/req)
IO depths: 1, 2, 4, 8, 16, 32, 64 ← 队列深度分布
看 KB/req 一列:4.9 KB/req 说明平均每次读只有 5KB,典型的随机 4K/8K 页读取。配合 IO depths 看队列深度分布,能知道"到底排了多深的队"。
用 seektime 工具测盘真实延迟
如果怀疑机械盘真的坏了(而不是被 IO 打满),seektime 能测出盘的随机寻道延迟:
max 380ms、95th 220ms 这种分布,说明这块机械盘的随机寻道已经彻底退化(正常应在 8-12ms 中位数)。这不是调优能救的,是盘到了物理寿命或性能极限。
一个更轻量的替代:iostat 的 svctm 与 await 比值
blktrace 要装工具、要 root、要抓文件,生产上有时不便。有个轻量近似:await(总等待)与 svctm(服务时间)的比值 ≈ 队列因子。await 远大于 svctm 说明请求大量在排队而非被服务。用这个能快速判断"是盘慢还是队列堵",不需要 blktrace。
随机 vs 顺序的一个反直觉结论
很多人以为"随机 IO 一定比顺序 IO 慢",其实只在机械盘上成立。对 SSD/NVMe,随机和顺序的差距大幅缩小:
| 盘种 | 随机 4K 读 IOPS | 顺序读带宽 | 差距 |
|---|---|---|---|
| HDD | 150 IOPS | 150MB/s | 随机比顺序慢约 1000 倍(寻道) |
| SATA SSD | 80K IOPS | 500MB/s | 差距缩小到个位数 |
| NVMe | 500K IOPS | 3.5GB/s | 几乎无差距 |
结论:判断"随机 vs 顺序"的紧迫性,要分盘种。机械盘上随机读是灾难,必须处理;NVMe 上随机读不是问题,别为它焦虑。这又回到阈值矩阵——判读标准必须跟着硬件走。
用 iostat 的 rareq-sz 快速估算"盘还能撑多少 IOPS"
这是我在事故三里用到的实用心算:
# 机械盘随机 IOPS 上限估算
# 随机读延迟约 8-12ms(含寻道),单次 4K
echo "机械盘随机 IOPS 上限 ≈ 1000 / 平均寻道时间(ms) ≈ 1000/10 = 100 IOPS"
所以当你看到 r/s 达到 5000,而机械盘理论上限只有 100-200,需求超上限 25-50 倍,这已经不只是"慢",是"物理上不可能完成"。看到这个数量级,就别纠结调优,直接上索引或换 SSD。
七、cgroup v2 blkio:容器/进程组级 IO 监控与限流
前面都是整机视角。现在大部分负载跑在容器里,容器之间互相抢 IO 是"症状 5:单一进程 vs 全局"的最常见来源。一个容器的备份脚本,能把同一宿主机上所有容器拖垮,而 iostat 只看得到 sda 100%,看不到是谁。
cgroup v2 把块 IO 控制器合并到 io 子目录,配合 memory 一起管。注意:cgroup v2 的 IO 控制器路径是 io.max/io.weight,不是 v1 的 blkio.*。
7.1 看容器真实 IO
# 找到容器 cgroup(以某容器 cid 为例)
ls /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.stat
# 看 IO 统计(rbytes/wbytes 累计,rrios/wrios 次数)
cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.stat
rbytes/wbytes:累计读写字节rios/wrios:累计读写次数rbytes / rios= 平均请求大小,用来判断容器是随机还是顺序(和整机rareq-sz一个道理)
更省事的做法:docker stats 或容器平台自带监控,但它们往往只看 CPU/内存,对 IO 只有带宽没有队列延迟,判断"容器是不是 IO-bound"不够。要看延迟,还得回到 iostat。
7.2 用 io.max 限流(关键操作)
限制某个容器的读带宽(写同理用 rbps/wbps,IOPS 用 riops/wiops):
# 限读 100MB/s、写 50MB/s
echo "8:0 rbps=104857600 wbps=52428800" > /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.max
# 限制 IOPS(随机小 IO 场景限 IOPS 比限带宽有效)
echo "8:0 riops=1000 wiops=500" > /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.max
# 查看当前限制
cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.max
为什么随机小 IO 要限 IOPS 而不是带宽:机械盘随机 4K 读每秒顶多 200 次寻道,就算你限了带宽 100MB/s,4K 小请求照样能把盘打满寻道。限 IOPS 直接卡住"寻道次数",这才是机械盘的命门。
7.3 用 io.weight 做权重分配(不硬限,按优先级分)
硬限流(io.max)适合"绝对不能超"的场景。如果是"想保证核心容器优先、但别让闲容器闲死",用权重:
# 权重范围 1-10000,默认 100。核心库设 500,备份容器设 10
echo 500 > /sys/fs/cgroup/system.slice/docker-<db>.scope/io.weight
echo 10 > /sys/fs/cgroup/system.slice/docker-<backup>.scope/io.weight
权重不会硬性封顶,它只在有竞争时按比例分带宽。核心库权重 500、备份权重 10,在争抢时大约 50:1 分,但备份容器独处时依然能用满。这比硬限流更符合"业务弹性"。
7.4 memory.max 与 IO 的联动(易忽略的坑)
内存和 IO 是联动的一对:memory.max 设小了,容器会频繁回收页缓存 → 触发脏页回写 → 压垮 IO。这正好对应"症状 6:缓存回写压垮盘"。
cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/memory.max
cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/memory.events
memory.events 里的 oom_kill、max 计数能告诉你容器是不是被内存逼到墙角。如果 oom_kill 在涨,容器 IO 慢的根因可能是内存不足导致疯狂回写,而不是磁盘真的坏了。 这类问题加磁盘没用,得调 memory.max 或 vm.dirty_ratio。
用 BPF 或容器平台看实时 IO 分布(进阶)
io.stat 是累计值,想实时看每个容器的 IO 速率,可以差分采样:
# 两次采样相减得速率(rbytes 增量 / 时间间隔)
watch -n 2 'cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.stat'
如果机器上有 BPF 工具(bcc/bpftrace),biopattern 能直接告诉你每个设备的随机 vs 顺序比例,biotop 能按进程实时排名 IO,比 iotop 更底层(直接从块层抓,不依赖进程 IO 统计):
%RNDR 95% 告诉你:这块盘 95% 的读是随机的,机械盘必死。这是"症状 4:随机小 IO vs 顺序大 IO"最直接的数据。
cgroup v2 一个常踩的坑:IO 控制器没启用
有些内核发行版默认没启用块 IO 控制器,io.max 写了也不生效。先验证:
# 写之前看当前限制(如果提示 No such file,说明控制器没挂)
cat /sys/fs/cgroup/system.slice/docker-<cid>.scope/io.max
# 检查内核启动参数有没有 enable 块 IO 控制器(cgroup_no_io 会禁用它)
cat /proc/cmdline | grep -o 'cgroup_no_io[^ ]*'
如果控制器被禁用,需要在 grub 启动参数去掉 cgroup_no_io=1 并重启,io.max 才能生效。这是 cgroup v2 IO 限流最常见的不生效原因,写配置前先验证,别白忙活。
八、三种盘阈值矩阵:HDD/SSD/NVMe 判读差异
这是全文最反直觉、也最容易被忽视的一章。 很多运维拿着 iostat 的一套阈值(await > 20ms 就报警)去套所有盘,结果 NVMe 天天误报、机械盘反而漏报。三种盘的判读阈值完全不同,不能用一个模板。
8.1 阈值矩阵
| 指标 | 机械盘 HDD | SATA SSD | NVMe |
|---|---|---|---|
| %util 判读 | 有效,> 80% 即饱和 | 需结合带宽 | 基本无效(假饱和陷阱) |
| await 正常 | 5-15ms | 0.1-0.5ms | 0.02-0.1ms |
| await 慢 | > 20ms | > 2ms | > 0.5ms |
| await 爆 | > 200ms | > 20ms | > 5ms |
| r_await 关键 | 随机读 > 100ms 即崩 | > 10ms 异常 | 看 rMB/s 更准 |
| 额定 IOPS | 100-200(随机) | 20K-100K | 500K-1M |
| 额定带宽 | 100-250MB/s | 500MB/s | 3-7GB/s |
| 主要瓶颈 | 寻道(随机 IO) | 控制器/磨损 | 主机接口/队列深度 |
| 判读核心 | await + 随机小 IO | await + 带宽 | 带宽对比额定值 |
8.2 机械盘(HDD):await 是命门,随机 IO 是敌人
await > 200ms才叫真爆,20-100ms 是"慢但还能用"- 机械盘死穴是随机小 IO:一次 4K 随机读要 8-10ms 寻道,每秒顶多 100-200 次,IOPS 是硬上限。
rareq-sz < 16K且r/s高 = 盘在寻道风暴里 - 机械盘
await > 200ms+r_await > 100ms,基本可以宣布这块盘已到极限,再好的调优也救不回,只能换 SSD/NVMe 或改随机为顺序
8.3 SATA SSD:await 和带宽都要看
await > 2ms已算异常,> 20ms基本在等控制器- SSD 随机读没有寻道惩罚,IOPS 能到几万,但带宽上限约 500MB/s。
rkB/s接近 500MB/s 且await涨 = 接近带宽上限 - SSD 还有个隐性坑:写入放大。4K 随机写会先写整个 4K 页但 SSD 以块为单位擦写,实际物理写比逻辑写大好几倍。
w_await高而wkB/s不高,要考虑写入放大
8.4 NVMe:别信 %util,信带宽和延迟
NVMe 的 %util 100% 是最大的骗局。 因为 NVMe 队列极深(可达 64K 并发),设备永远"有请求在跑",%util 常年 100%——但它远没到极限。这是"症状 2:应用卡但盘不忙"的核心。
- NVMe 判读看
rkB/s/wkB/s是否接近额定带宽(如 3.5GB/s 的盘,实际到 3GB/s 才算接近上限) - NVMe 判读看
await:> 0.5ms已异常,> 5ms基本是队列堵了 %util 100%+await只有 0.1ms + 带宽只有 20% → 假饱和,设备好得很,业务慢是别的原因(见事故四)
8.5 一个必须记住的坑:iostat 的 %util 口径
%util 是设备有请求的时间占比,不是"设备利用率"。对单队列 HDD,100% 意味着真在忙;对多队列 NVMe,一个队列忙也会拉高 %util,但设备可能只用了 10% 的能力。判 NVMe 永远别单独看 %util,要看带宽 + await。
8.6 三种盘的实际判读示例
机械盘随机读打穿(数据库场景,见事故三):
Device r/s rkB/s r_await aqu-sz rareq-sz await %util
sda 5200.00 41600.00 218.00 56.30 8.00 215.00 100.00
读:rareq-sz 8(4KB 随机)+ r/s 5200 + r_await 218ms + %util 100% → 机械盘在 5200 IOPS 的随机读下彻底崩溃,每次读等 218ms。这种盘判读直接看 r_await 和 rareq-sz,一眼定罪。
SATA SSD 接近带宽上限(大文件顺序写):
Device w/s wkB/s w_await aqu-sz wareq-sz await %util
sdb 900.00 460000.00 12.00 5.20 512.00 12.10 98.00
写:wareq-sz 512(256KB 顺序写)+ wkB/s 460000(460MB/s)→ 接近 SATA SSD 的 500MB/s 带宽上限,w_await 12ms 偏高说明在带宽边缘挣扎。这种盘判读看带宽是否接近额定 500MB/s,而不是看 %util。
NVMe 假饱和(见事故四):
Device r/s rkB/s r_await aqu-sz rareq-sz await %util
nvme0n1 18000.00 800000.00 0.08 42.00 45.00 0.08 100.00
读:%util 100% 但 r_await 0.08ms + rkB/s 800000(800MB/s,额定 3.5GB/s 的 23%)→ 设备健康,远未到极限。%util 100% 只是"队列一直有活",不是饱和。这种盘判读看带宽利用率,%util 完全失效。
8.7 告警阈值怎么按盘种配
监控告警最容易踩"一套阈值走天下"的坑。给出按盘种配的参考(Prometheus/node_exporter 场景):
| 盘种 | 告警项 | 阈值 |
|---|---|---|
| HDD | await(iostat 采样) |
> 100ms warning,> 200ms critical |
| HDD | r_await |
> 100ms warning |
| SSD | await |
> 5ms warning,> 20ms critical |
| NVMe | 带宽利用率 | > 80% warning,> 95% critical |
| NVMe | await |
> 1ms warning,> 5ms critical |
| 任意 | 设备故障 | 盘掉线 / smartctl 报错 |
核心思想:HDD 用 await(延迟敏感),NVMe 用带宽利用率(吞吐敏感),SSD 两者折中。千万别给 NVMe 配 %util > 95% 告警——那会天天误报(事故四就是这么来的)。
九、5 个真实事故复盘
下面 5 个都是我亲手处理过的生产事故,按"症状 → 排查 → 根因 → 修复"完整还原。这章是全文的灵魂,建议对照前八章边看边回忆。
事故一:备份 rsync 打满盘
现象:某天早上业务反馈"所有接口都慢",load average 飙到 40 多,但 %us 只有 3%。
排查链:
vmstat 1 5 # b 列 14-18,wa 40%+,r 才 3-5 → IO 瓶颈不是 CPU
iostat -x 1 5 # sda %util 100%,await 165ms,rareq-sz 只有 10K → 随机读风暴
iotop -oPa # PID 1234 rsync DISK READ 48.5M/s,IO> 99.99% → 元凶锁定
根因:备份脚本在早上 9 点全量执行 rsync -a --delete /data/ /backup/,没有限速。rsync 每次都要打开、读取、比对 /data 里几十万个小文件,形成随机小读,把机械盘的寻道彻底打穿。await 165ms 就是证据——每次读都在等寻道。
修复:
# 1. 立刻限速,把影响降到最低(不中断备份的前提下)
ionice -c3 -p 1234 # 降到 best-effort 空闲级,只吃空闲 IO
# 或更直接限带宽
ionice -c3 rsync -a --delete --bwlimit=20000 /data/ /backup/
# 2. 永久修复:错峰 + 增量,别在业务高峰全量扫
# 定时任务从 09:00 挪到 02:00,并加 --bwlimit 限 20MB/s
经验:rsync -a --delete 不带 --bwlimit 是生产大忌。备份必须错峰、限速、增量。备份是生产事故的头号元凶,没有之一。
复盘补充:为什么不是 CPU 也不是网络?
当时值班同事先怀疑网络(因为"慢"),抓包半天没结果。我用一个命令就排除了网络:sar -n DEV 1 5 看到网卡流量正常,而 vmstat 的 b 列和 wa 列才是异常。判 IO 的诀窍:先看 b 列(阻塞进程数),它比任何网络抓包都直接。 备份这种"读大量小文件"的场景,瓶颈几乎必然在磁盘随机读,不在网。
事故二:swap 抖动,kswapd0 占顶
现象:一台 16G 的 Java 服务节点,业务偶发卡顿,top 里 kswapd0 经常排第一,CPU 显示 si(软中断)和 %sys 都偏高,iostat 的 sda 写压力大。
排查链:
free -h # available 只剩 800M,swap 已用 2G
vmstat 1 5 # si/so 持续非零(每 5 秒换出几百 MB)→ 在疯狂换页
top -b -n1 # kswapd0 常驻 CPU,%wa 不低
iostat -x 1 # sda w_await 高,wkB/s 波动大 → 换页写回压盘
根因:内存不足,内核 kswapd0 线程疯狂回收内存并把匿名页换到 swap。swap 本身在机械盘上,换页的随机写又反过来打盘,形成"内存不足 → 换页 → 磁盘更慢 → 进程等更久 → 内存堆积更多"的正反馈死循环。kswapd0 占 CPU 顶不是 CPU 问题,是内存+IO 的联动灾难。
修复:
# 1. 临时缓解:降低换页倾向(治标,别当万能药)
sysctl vm.swappiness=10
# 2. 看真实内存压力(psi 内存指标,>50% 即严重)
cat /proc/pressure/memory
# some avg10=68.00 avg60=42.00 avg300=20.00
# 3. 根治:加内存到 32G,或限制 Java 堆(-Xmx8g)
经验:kswapd0 占 CPU 顶,第一反应不该是杀它(它是内核线程杀不掉),而是看 /proc/pressure/memory 和 free available。swap 抖动 + IO 卡顿 = 内存不够,加内存比调任何 IO 参数都有效。
复盘补充:swap 放在机械盘是双重灾难
这台机器的 swap 分区恰好也在机械盘上。换页的随机写 + 业务随机读抢同一块盘,两边都慢。我当时的处理:把 swap 挪到一块闲置的 SSD 上(swapoff /dev/sda2 && mkswap /dev/nvme0n1p3 && swapon /dev/nvme0n1p3),抖动立减一半。教训:swap 该放 SSD/NVMe 不该放 HDD,机械盘做 swap 等于用寻道速度当内存速度。
另一个坑:vm.swappiness=10 我只说临时治标,是因为它只影响匿名页换出倾向,页缓存回收另有机制。swap 抖动的根治只有两个:够内存,或限制住吃内存的进程。 调 swappiness 只是把问题推迟,不是解决。
事故三:数据库随机 IO 压垮机械盘
现象:MySQL 主库在业务高峰期 load 冲高,查询普遍慢 3-5 秒,应用侧连接池打满。
排查链:
iostat -x 1 5 # sda %util 100%,await 185ms,r_await 220ms,rareq-sz 8K
# → 随机读 + 高延迟,机械盘被随机读打穿
iotop -oPa # 多个 mysqld 线程 IO> 80%,DISK READ 总和 40M/s
pidstat -d 1 3 # mysqld 的 kB_rd/s 只有 40M/s,但 iodelay 极高
关键判断:rareq-sz 只有 8K(4K/8K 的页读取),r/s 却有 5000+。机械盘每秒只能做 100-200 次寻道,这里却要求 5000 次随机读——需求超出物理能力 25 倍,神仙也救不了。
根因:数据库某张核心表没有合适的索引,全表扫描产生大量随机 4K/8K 页读取;且数据目录和日志目录在同一块机械盘上,日志的 fsync 写被随机读打断,延迟雪上加霜。
修复:
# 1. 先止血:加索引,把全表扫描变索引查找
ALTER TABLE orders ADD INDEX idx_user_created (user_id, created_at);
# 2. 中期:数据盘和日志盘分开(随机读和顺序写分家)
# /var/lib/mysql(数据) 与 /var/log/mysql(日志)分到不同物理盘
# 3. 长期:换 SSD/NVMe。机械盘不该承载高并发随机读的数据库
经验:数据库随机 IO 压垮机械盘,本质是"随机读 IOPS 需求 >> 机械盘物理上限"。 加索引(降 IOPS 需求)和换 SSD(提 IOPS 供给)是两条腿,只做一条都不够。别只加内存,内存大只会把脏页留在内存,随机读照样打盘。
复盘补充:一个数字戳穿"加内存"的幻想
当时有同事建议"把 buffer pool 从 8G 加到 32G,随机读就进内存了"。我用一个数字否了:iostat 显示 r/s 5200,即每秒 5200 次随机读,假设都在 buffer pool 里,需要每秒喂进 5200 个新页——buffer pool 只能缓存"重复读"的页,全表扫描的页是一次性的,缓存了也没用。只要 SQL 是全表扫描(无索引),加多少内存都白搭。先加索引把 r/s 从 5200 降到 200,才是正解。
事故三补充:用 SQL 定位到"哪条语句在扫表"
IO 是表象,SQL 才是根因。MySQL 定位:
-- 看当前在执行的语句,重点找没走索引的
SHOW FULL PROCESSLIST;
-- 确认全表扫描的查询(type 列 = ALL)
EXPLAIN SELECT * FROM orders WHERE status = 'PENDING';
id select_type table type possible_keys key rows Extra
1 SIMPLE orders ALL NULL NULL 12000000 Using where
type = ALL + rows 12000000,就是它——1200 万行全表扫描,每行都触发一次随机页读。加了 idx_user_created 后 type 变 ref,rows 降到几百,r/s 立刻从 5200 掉到 200 以下。这一步才是治本,换盘只是给"乱扫"买单。
事故四:NVMe %util 100% 但不慢(假饱和)
现象:一台跑高并发 API 的 NVMe 服务器,iostat 显示 %util 100%,报警系统疯狂告警"磁盘饱和",但业务延迟正常、吞吐正常。值班同事差点按"磁盘满"去扩容。
排查链:
iostat -x 1 5 # nvme0n1 %util 100%,await 只有 0.08ms,rkB/s 只有 800M
# → %util 100% 但 await 极低、带宽仅 25% → 假饱和!
关键判断:这块盘额定带宽 3.5GB/s,实际只用了 800M(约 23%);await 只有 0.08ms,完全健康。%util 100% 只是"设备有请求在队列里",不代表设备满了。 NVMe 队列极深,设备永远显示忙,但根本没到极限。
根因:这是监控误报,不是故障。 告警阈值用了机械盘的 %util > 95% 模板去套 NVMe,必然误报。业务不慢,因为 NVMe 的 100% util 里其实有大量空闲等待。
修复:
# 给 NVMe 单独建告警规则:用带宽 + await,别用 %util
# Prometheus 示例:NVMe 用"带宽利用率"而非 %util 判断
(rate(node_disk_read_bytes_total{device="nvme0n1"}[5m])
+ rate(node_disk_written_bytes_total{device="nvme0n1"}[5m]))
/ 3.5e9 * 100 > 80
经验:别用一套 %util 阈值监控所有盘。 NVMe 判饱和要用"带宽利用率"和"await",%util 对 NVMe 基本是废指标。这是"症状 2:应用卡但盘不忙"的典型,也是监控配置最常见的坑。
复盘补充:报警风暴怎么快速压下去
当时告警系统因为 %util 100% 疯狂发短信,值班同事准备加机器。我做的第一件事不是调告警,而是加了一列 await 到告警规则里:只有 %util 高 AND await 高 才触发。因为 await 才是"请求等得久"的直接证据,%util 只是"设备有活"。改成:
# Prometheus 规则:%util 高 且 await 高 才算真饱和
expr: |
(node_disk_io_time_seconds_total{device=~"nvme.*"}
/ node_disk_io_time_weighted_seconds_total{device=~"nvme.*"}) > 0.9
and
(node_disk_io_time_seconds_total{device=~"nvme.*"} / 5 > 5)
虽然 PromQL 写起来绕,但思路就一句:NVMe 的"饱和"必须同时满足"忙"和"慢",缺一不可。 从那以后这个集群再没误报过。
另一层反思:为什么当时没人早发现?因为监控模板是从一台机械盘服务器复制过来的,%util > 95% 对机械盘合理、对 NVMe 是灾难。监控配置要跟着硬件走,设备升级了,告警阈值不升级等于没监控。
事故五:ext4 data=journal 日志翻倍
现象:一台 ext4 文件系统的应用服务器,iostat 显示 w_await 高、%util 不低,iotop 里看到大量 flush-8:0(内核回写线程)和 jbd2(ext4 日志线程)在写。应用每次写文件都明显变慢。
排查链:
iostat -x 1 5 # sda %util 90%,w_await 45ms,wkB/s 并不高 → 写慢但带宽不大
iotop -oPa # flush-8:0 和 jbd2/sda-8 占顶,写多
cat /proc/mounts | grep /data # /data 是 ext4
dumpe2fs -h /dev/sda1 | grep -i journal # 看日志模式
根因:/data 用 ext4 且挂载为 data=journal(默认其实多半是 data=ordered,但有些老系统或手工挂载用了 data=journal)。data=journal 模式会把数据和元数据都写两遍:先写日志,再写数据——等于写放大 2 倍。应用每次 fsync 要等两次落盘,w_await 自然高。
修复:
# 1. 改回 ordered(推荐:元数据先写日志,数据先落盘,只写一遍)
mount -o remount,data=ordered /data
# 持久化:改 /etc/fstab 里对应行 data=journal → data=ordered
# 2. 如果实在要 data=journal(某些崩溃一致性场景),至少把日志放独立分区
# journal 放 SSD,数据放 HDD,日志写不再拖累数据盘
经验:w_await 高但 wkB/s 不高,是"写放大"的典型信号。 除了 ext4 data=journal,还有 SSD 写入放大、btrfs COW、以及 fsync 调用过频(每次提交都强制落盘)。先看文件系统挂载参数,再查应用 fsync 频率,别一上来就换盘。
复盘补充:data=journal vs data=ordered 到底差多少
我在这台机器上做了实测对比(fio 顺序写 4K 随机写):
| 挂载模式 | 随机 4K 写 IOPS | 顺序写带宽 | 说明 |
|---|---|---|---|
data=journal |
320 | 120MB/s | 数据和元数据都写日志,双倍写 |
data=ordered |
780 | 190MB/s | 元数据先写日志,数据直接落盘 |
data=writeback |
1100 | 210MB/s | 最激进,崩溃一致性最弱 |
data=journal 比 data=ordered 随机写慢了一半不止,因为它先写日志再写数据,同一份数据落两次盘。改回 data=ordered 后 w_await 从 45ms 降到 12ms。这就是"写放大 2 倍"的实锤。
注意:data=journal 不是一无是处,它崩溃一致性最强(掉电不丢数据)。取舍在于业务是否能容忍 fsync 慢。数据库场景几乎都该用 ordered,除非有明确的"掉电必须零丢失"要求。
十、缓解手段:从限速到换盘的 5 个台阶
定位到根因后,缓解手段要按"成本从低到高、见效从快到慢"来选。我给出一套实操组合。
10.1 降级 / 限速(最快,临时止血)
# 让进程只在系统空闲时才用 IO(best-effort 空闲级)
ionice -c3 -p <pid>
# 永久:让备份/批处理进程天生低优先级
ionice -c3 rsync -a --delete /data/ /backup/
# 限进程 IO 带宽(rsync 限 20MB/s)
ionice -c3 rsync --bwlimit=20000 /data/ /backup/
# 看进程当前 ionice 等级
ionice -p <pid>
ionice -c3(idle 级)是运维最被低估的武器:它只在磁盘完全空闲时才让进程发 IO,业务高峰时它自动让路,低谷时又不会饿死。备份、日志聚合、离线分析都该用。
10.2 内核参数调优(系统性,需压测)
# 脏页阈值:默认 20%,调低让回写更平滑,避免周期性写风暴
sysctl vm.dirty_ratio=10
sysctl vm.dirty_background_ratio=3
# 脏页驻留时间:默认 30 秒,调低加速回写,减少脏页堆积
sysctl vm.dirty_expire_centisecs=3000
# swap:数据库/延迟敏感应用降低换页倾向
sysctl vm.swappiness=10
注意:dirty_ratio 调低会让回写更频繁(每次更少),能平滑"周期性写峰",但总写入量没变。机械盘场景配合 ionice 才有意义,别指望单改这个参数就解决随机读问题。
10.3 布局调整:分盘、错峰、合并
- 数据盘 / 日志盘分离:随机读(数据)和顺序写(日志)别同盘,机械盘尤其要分
- 备份错峰:从业务高峰挪到低谷(02:00-05:00)
- 批量 IO 合并:把零散小写聚合成批量大写,减少
fsync次数(应用层做,或用缓冲层)
10.4 cgroup v2 限流(容器场景)
见第七章:io.max 硬限(随机小 IO 限 riops),io.weight 软分权。容器场景这套比整机 ionice 更可控。
10.5 硬件升级(最后手段,治本)
- 机械盘 → SATA SSD:随机读 IOPS 从 200 提到 5 万,随机读场景质变
- SATA SSD → NVMe:带宽从 500MB/s 提到 3-7GB/s,顺序大 IO 场景质变
- 数据库:
data与binlog/redo分盘,redo log 放 NVMe
决策顺序:先 ionice 止血(5 分钟)→ 再查根因改配置(30 分钟)→ 再考虑 cgroup 限流或分盘(半天)→ 最后才谈换硬件。很多人一上来就申请买盘,结果根因是 data=journal 写放大,换了 NVMe 也没用。 先定性再花钱。
10.6 一个常被忽略的止血点:sysctl 的 dirty 参数别瞎调
很多人一看到 IO 慢就调 vm.dirty_ratio,但调错了反而更糟:
dirty_ratio调大 → 脏页攒得更多,回写时一次性压垮盘(周期性写峰更狠)dirty_ratio调小 → 回写更频繁,但每次量少,平滑了写峰,代价是 CPU 和写放大略增
对机械盘(写峰值敏感),调小 dirty_ratio 有用;对 NVMe(带宽大),调小反而损失吞吐。调之前先明确目标:是平滑写峰还是最大化吞吐,方向相反。
10.7 ionice 的完整分级速查
ionice 分三种类,别用错:
# -c1 Realtime:实时,会抢占普通 IO(慎用,只有内核/关键进程才配)
ionice -c1 -p <pid>
# -c2 Best-effort:尽力而为,默认 n=4,可调 0-7(0 最高)
ionice -c2 -n 7 -p <pid> # 备份进程降级,让业务优先
# -c3 Idle:空闲级,只在磁盘完全空闲时才发 IO(最安全,推荐备份用)
ionice -c3 -p <pid>
# 看当前值
ionice -p <pid>
实操经验:ionice -c3(idle)对备份类任务几乎无副作用——业务高峰它自动停,业务低谷它补跑。-c1(realtime)是双刃剑,用错会把业务进程的 IO 全部饿死,除非明确知道自己在干什么,否则别碰 -c1。
10.8 分盘规划实操
分盘不是随便 mkdir,要按IO 特征分:
# 1. 数据库:数据(随机读) / binlog+redo(顺序写) / 临时表(高随机写) 三盘分离
/data # 数据文件,随机读为主 → 放 SSD
/redo # redo log + binlog,顺序写为主 → 放 NVMe
/tmp/mysql # tmpdir,临时排序文件,高随机写 → 放 SSD
# 2. 应用:日志(顺序写) / 程序(读) / 临时(写) 分离
/var/log/app # 应用日志,顺序追加写 → 放 NVMe(fsync 快)
/app/data # 业务数据 → 放 SSD
/app/tmp # 临时文件 → 放 SSD
原则:顺序写(日志/redo)和随机读(数据)永远分盘。机械盘场景这是救命稻草;NVMe 场景收益小一点,但容器镜像层和数据库日志仍建议分开。
10.9 换盘前先做一次"成本-收益"判断
别盲目换盘,用数据说话。换盘收益 = 当前瓶颈在不在"盘"上:
上面第二种情况,业务慢的根因可能是 CPU、锁、网络或应用代码。先确认瓶颈确实在盘,再花买盘的钱。 这是运维最该守住的一条纪律。
十一、10 条速查命令
| # | 命令 | 用途 |
|---|---|---|
| 1 | vmstat 1 5 |
60 秒定性:r/b/wa 判 CPU 还是 IO |
| 2 | iostat -x 1 5 |
设备级:%util/await/aqu-sz/rareq-sz |
| 3 | iostat -xm 1 5 |
按设备+分区看(-m 单位 MB) |
| 4 | iotop -oPa |
进程级:揪 IO 元凶 |
| 5 | pidstat -d 1 5 |
进程级兜底:kB_rd/s + iodelay |
| 6 | ps -eo state,pid,cmd,wchan:30 \| awk '$1~/D/' |
找 D 状态进程 + 内核函数 |
| 7 | blktrace -d /dev/sda -o /tmp/bt -w 5 |
块层抓取(随机 vs 顺序) |
| 8 | cat /sys/fs/cgroup/*.scope/io.stat |
cgroup 容器真实 IO |
| 9 | echo "8:0 rbps=104857600" > .../io.max |
cgroup v2 限流 |
| 10 | ionice -c3 -p <pid> |
进程降 IO 优先级 |
组合套路(5 分钟内定位):
vmstat 1 5 # 1. 定性:b/wa 高 → IO 问题
iostat -x 1 5 # 2. 定盘:哪块盘 + 随机/顺序 + 延迟
iotop -oPa # 3. 定进程:谁在作恶
ps -eo state,pid,cmd,wchan:30 | awk '$1~/D/' # 4. 看是否 D 状态卡死
从症状到结论的 3 分钟实战流
把全文浓缩成一条可背下来的判断流,遇到 IO 现场直接照做:
第 1 分钟:load 高 + CPU 闲?
├─ yes → vmstat 看 b/wa → b 高 → 是 IO 瓶颈
└─ no → 去看 cpu / memory 监控
第 2 分钟:哪块盘 + 什么 IO?
├─ iostat -x → rareq-sz < 16K → 随机小 IO(HDD 大忌)
├─ iostat -x → rareq-sz > 64K → 顺序大 IO(相对安全)
└─ %util 100% 但 await 低 → NVMe 假饱和,看带宽
第 3 分钟:谁在作恶?
├─ iotop -oPa → IO> 30% 的进程 → 元凶
├─ pidstat -d → iodelay 大但 kB_rd 小 → 受害者,找别人
└─ ps D 状态多 → wchan 定位内核函数
一句话总结全文:vmstat 定性(是 IO 不是 CPU),iostat 定盘(哪块、随机还是顺序),iotop/pidstat 定进程(谁是元凶),blktrace 定细节(队列与合并),阈值分盘种(HDD 看 await、NVMe 看带宽),容器用 cgroup v2 限流。抓住这六点,磁盘 IO 瓶颈逃不出你的手心。
FAQ
Q1:load 很高但 CPU 使用率很低,为什么?
A1:八成是 iowait 在作怪。load 统计的是可运行 + 不可中断(D 状态)睡眠进程。进程在等磁盘 I/O 时进入 D 状态,照样计入 load,但 CPU 是空的。用 vmstat 看 b 列非零、wa 列偏高,再 iostat 查 %util/await 定位到具体盘,方向就清楚了。别去加 CPU,那是给错误的病开药。
Q2:iostat 的 %util 100% 但业务不慢,正常吗?
A2:NVMe 或 SSD 可能出现这种情况。%util 是"设备有请求在跑的时间占比",NVMe 队列极深,设备常年显示 100%,但实际带宽只用了 20%、await 只有 0.1ms——这叫假饱和。判 NVMe 要看带宽利用率(实际 rMB/s ÷ 额定带宽)和 await,别单独看 %util。
Q3:iowait 高但 iostat %util 不高,怎么解释?
A3:通常是多队列设备、或 NFS/网络文件系统。%util 按设备聚合,并发请求多时单个请求等待长,但设备本身没满。用 iotop/pidstat 定位到具体进程,或用 blktrace 看真实队列深度。也可能是 D 状态进程堆积,用 ps -eo state,wchan 确认。
Q4:D 状态进程太多,kill 不掉怎么办?
A4:D 状态是内核不可中断睡眠,等 I/O 完成,SIGKILL 无效。先看 wchan 停在哪个函数:io_schedule 是盘慢还能等;blk_mq_submit_bio 是队列堵;如果 iostat 数值不刷新、D 进程越积越多,是盘挂了,只能重启。别反复 kill,处理根因:等盘恢复或重启。
Q5:rsync 备份经常打满磁盘,怎么限速?
A5:用 ionice -c3 给 best-effort 空闲级,备份自动让路业务;或 rsync --bwlimit=20000 限 20MB/s。更彻底的是错峰到业务低谷(02:00-05:00),并确认是增量而非每次全量扫描。备份不限速是生产事故头号原因。
Q6:swap 抖动(kswapd0 占 CPU 顶)怎么处理?
A6:内存不足导致内核换页线程疯狂工作。临时 sysctl vm.swappiness=10 降换页倾向(治标),根治是加内存或限制进程内存(-Xmx/cgroup memory.max)。看 /proc/pressure/memory 的 some avg10,> 50% 即内存真实压力。kswapd0 是内核线程,别杀,处理内存根因。
Q7:机械盘 await 200ms 了,是不是必须换盘?
A7:机械盘 await > 200ms 基本是寻道风暴或盘接近损坏。先看 iostat 的 rareq-sz:若 < 16K 且 r/s 高,是随机小 IO 打穿寻道,换 SSD/NVMe 是唯一解法(机械盘随机 IOPS 硬上限 200,物理限制改不了)。若 rareq-sz 大是顺序 IO,那可能是盘坏了,先 smartctl 查坏道。
Q8:数据目录和日志目录要不要分盘?
A8:要。随机读(数据)和顺序写(日志)混在一个机械盘会互相干扰寻道:一次日志 fsync 可能被随机读打断,延迟飙升。分盘后两者互不干扰,数据库日志 fsync 延迟明显下降。数据库 data/binlog、容器镜像层与数据层,分盘效果最明显。