监控面板上磁盘 %util 长期 100 %,值班同学据此申请换 NVMe。换完盘,util 还是 100 %,应用延迟一点没变。复盘时才发现:真正的瓶颈是每次写都 fsync,而 IO 大小只有 4 KB——盘一直都很忙,但忙的不是"能力上限",是"根本喂不饱"。
util 是这几个指标里最容易被误读的一个:它统计的是"设备有请求在处理的时间占比",与设备是否达到能力上限无关。一块 NVMe 的 util 可以长期 100 % 而仍然有大量余量;反过来,util 只有 30 % 也可能是真饱和(队列在等)。
1. 五个指标各自回答什么
| 指标 | 含义 | 它能回答 | 它答不了 |
|---|---|---|---|
r/s、w/s |
每秒完成的 IO 次数(IOPS) | 操作频率 | 每次多大 |
rkB/s、wkB/s |
吞吐 | 带宽用了多少 | 延迟如何 |
await |
平均每次 IO 的总耗时(排队 + 服务) | 用户感知的慢 | 慢在哪一段 |
%util |
设备有请求在处理的时间占比 | 设备在忙 | 是否已到上限 |
aqu-sz |
平均队列深度 | 有没有排队 | 排的是哪类 IO |
// 两组真实读数,同样 100%,结论相反
util=100% await=0.3ms aqu=0.9 → 串行小 IO,盘远没到顶
util=100% await=12ms aqu=6.2 → 真在排队,拐点已经过了
// 所以看到 100% 的第一件事是看另外两个数,不是写采购单
第四行与第五行的区别是全篇的关键:util 说"忙",aqu-sz 说"堵"。忙不等于堵——一个只有一次 IO 的 4 KB 请求也能让 util 那一刻变 100 %。
2. 两种"假饱和"
| 现象 | 组合特征 | 真实原因 |
|---|---|---|
| 小 IO 密集 | util 高、await 低、IOPS 高、吞吐低 |
每次只搬几 KB,设备被唤醒次数撑满 |
| 同步串行 | util 高、await ≈ 单次服务时间、aqu-sz ≈ 1 |
一次只有一个请求在途(fsync、串行写) |
| 预读无效 | 吞吐上不去、r/s 高但 rkB/s 低 |
读的是随机小块 |
| 真饱和 | util 高、await 上升、aqu-sz > 1 且随并发涨 |
设备确实到上限了 |
第二行是数据库与日志类应用最常见的情况:fsync 让每次写都变成"发一个、等一个",队列深度永远接近 1。此时的 util 100 % 描述的是"等盘的时间占比",而不是"盘的带宽用完了"——换更快的盘能改善,但先合并写(批量提交、组提交)收益更大。
// 一条命令先分辨"真饱和"还是"假饱和"
$ iostat -x 1 3 | awk 'NR>3 {print $1, "util="$NF, "await="$10, "aqu="$11}'
// 判据:aqu ≈ 1 且 await 小 → 串行小 IO;aqu > 2 且 await 涨 → 真在排队
3. 真正的饱和信号:加并发后吞吐不涨
判断 IO 是否到顶,最可靠的办法不是看瞬时值,而是看"加压后的响应":
| 加压后的表现 | 结论 |
|---|---|
吞吐随并发线性涨、await 略升 |
还有余量 |
吞吐持平、await 线性涨 |
已到拐点 |
吞吐下降、await 暴涨 |
过载,需要退并发 |
吞吐与 await 都乱跳 |
干扰源(同机其他业务 / 后台任务) |
// 加压找拐点:同机加并发,看吞吐还涨不涨
$ for d in 1 4 16; do fio --name=t --rw=randwrite --bs=4k --iodepth=$d \
--runtime=10 --filename=/tmp/fio.t --output-format=json 2>/dev/null \
| python3 -c "import json,sys;j=json.load(sys.stdin)['jobs'][0]['write'];print('$d','iops',int(j['iops']),'clat_ms',round(j['clat']['mean']/1000,2))"; done
// 拐点 = IOPS 不再涨、而 clat 开始线性涨的那一档
第二行就是 IOPS 上限的位置。这个拐点只能通过"加压测试"或"观察业务峰值"得到,无法从单张监控图看出来——所以"util 100 % 就要换盘"这个判断缺了最关键的一步。
// 同一个应用,两种盘上的拐点(4 KB 随机写)
SATA SSD: 2 万 IOPS 后 await 从 0.2ms 涨到 3ms
NVMe: 18 万 IOPS 后 await 从 0.05ms 涨到 1ms
// 换盘确实把拐点推后 9 倍;但如果业务只跑到 1 万 IOPS,
// 换盘不会带来任何收益 —— 这就是前面那次"换了没变快"
4. 三种 IO 形态,优化手段完全不同
| 形态 | 特征 | 优化方向 |
|---|---|---|
| 顺序大块(> 128 KB) | 吞吐导向,await 与块大小同阶 |
加带宽、调预读 |
| 随机小块(4–16 KB) | IOPS 导向,util 极易打满 |
合并、缓存、换 NVMe |
同步等待(fsync) |
aqu-sz ≈ 1,await ≈ 单次延迟 |
组提交、批量、异步刷盘 |
第三行的优化收益最大也最容易被跳过:把 N 次 fsync 变成 1 次,等于把 IOPS 需求降到 1/N。而"换更快的盘"只是把那 N 次做得更快,需求没变。
// 同一份写入负载,两种提交方式的成本
逐条提交: 1000 次 fsync × 0.5ms = 500ms,IOPS = 2000/s
组提交(100): 10 次 fsync × 0.5ms = 5ms,IOPS = 200/s(吞吐反而更高)
// 关键:IOPS 需求下降 10 倍,而设备没变
5. 别把"盘慢"当成唯一解释
| 现象 | 可能不是 IO | 怎么验证 |
|---|---|---|
| 应用写延迟高 | 锁等待、连接池满 | 看线程转储(BLOCKED) |
| 读大文件慢 | page cache 未命中 + 网络 | 看缓存命中率与网卡 |
| 数据库慢查询 | 执行计划、锁 | 看 EXPLAIN 与 innodb_trx |
| 容器内 IO 慢 | cgroup 的 io.max 限流 |
看 io.stat 的 throttled |
| 整机都慢 | 内存不足触发回收 | 看 si/so 与 pgscan |
第四行在容器环境里非常常见:宿主盘很快,但容器的 io.max 把 IOPS 限死了,于是容器内看到 await 很高而宿主 util 很低。这类问题换盘无效,只能调 cgroup 配额。
6. 排错:五种"以为要换盘"
| 现象 | 八成原因 | 处理方向 |
|---|---|---|
util 100 % 但应用不慢 |
小 IO 撑满唤醒次数 | 合并 IO,不用换盘 |
util 100 % 且 await 高 |
真到 IOPS 拐点 | 换盘或降并发 |
await 高但 util 低 |
队列在等(并发过大) | 降并发或加队列深度上限 |
| 只有写慢、读正常 | fsync/日志刷盘 |
组提交 |
| 容器内慢、宿主正常 | cgroup IO 限流 | 调 io.max |
第三行很反直觉:await 高而 util 低,说明请求在队列里等,而设备没忙满——常见原因是排队策略(如 mq-deadline 的调度延迟)或单个慢请求拖住队列。此时换盘通常无效。
7. 边界:什么情况下"利用率高"是正常的
| 场景 | 正常吗 | 说明 |
|---|---|---|
| 日志盘持续写入 | 正常 | 顺序写,await 低即可 |
| 备份/压缩任务期间 | 正常 | 有窗口限制,不必优化 |
| NVMe 长期 100 % | 正常 | 多队列设备没有"磁头"概念 |
| 小文件服务 | 正常 | IOPS 密集是业务形状 |
| 数据库 WAL 盘 | 正常 | 只要 await 在毫秒级 |
判断口径一句话:util 只说明"设备在忙",判断是否到顶要看"加压后吞吐还涨不涨"——所以正确的顺序是"先量业务的实际 IOPS 与块大小,再决定要不要换盘",而不是"看到 100 % 就换"。而在换之前,先问一句"这些 IO 能不能合并"。
8. 自测
%util统计的是什么?为什么 NVMe 可以长期 100 % 而仍有大量余量?await与util分别回答什么问题?"util 高 await 低"与"util 低 await 高"各说明什么?- 判断 IO 是否到上限的可靠方法是什么?为什么单张监控图看不出拐点?
- 同步
fsync为什么会把util推高?组提交为什么能同时降低 IOPS 需求与延迟? - 容器内 IO 慢但宿主
util低,最可能是什么原因? - 什么情况下"利用率高"完全正常,不必优化?
9. 一句话总结
util 说"设备在忙",aqu-sz 说"有没有堵",await 才说"用户会不会慢"——三者必须一起看。判断是否到上限的唯一可靠方法是加压后看吞吐是否还涨;而"util 100 %"既可能是真饱和(随机小块到 IOPS 拐点),也可能只是小 IO 或同步 fsync 把设备唤醒次数撑满。优化顺序:先合并 IO(组提交/批量)→ 再看并发 → 最后才换盘,反过来做就是那次"换了盘还是 100 %"。