2277 字
约 7 分钟
11
S15-A28 · 磁盘 IO:为什么"util 100 %"不等于"盘不行"

监控面板上磁盘 %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. 自测

  1. %util 统计的是什么?为什么 NVMe 可以长期 100 % 而仍有大量余量?
  2. await 与 util 分别回答什么问题?"util 高 await 低"与"util 低 await 高"各说明什么?
  3. 判断 IO 是否到上限的可靠方法是什么?为什么单张监控图看不出拐点?
  4. 同步 fsync 为什么会把 util 推高?组提交为什么能同时降低 IOPS 需求与延迟?
  5. 容器内 IO 慢但宿主 util 低,最可能是什么原因?
  6. 什么情况下"利用率高"完全正常,不必优化?

9. 一句话总结

util 说"设备在忙",aqu-sz 说"有没有堵",await 才说"用户会不会慢"——三者必须一起看。判断是否到上限的唯一可靠方法是加压后看吞吐是否还涨;而"util 100 %"既可能是真饱和(随机小块到 IOPS 拐点),也可能只是小 IO 或同步 fsync 把设备唤醒次数撑满。优化顺序:先合并 IO(组提交/批量)→ 再看并发 → 最后才换盘,反过来做就是那次"换了盘还是 100 %"。

S15-A28 · 磁盘 IO:为什么"util 100 %"不等于"盘不行"
https://www.clxhxhhr.top/posts/6749/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。