机器看着不忙,top 里 CPU 才 20%,内存也还有富余,但接口就是慢,数据库简单查询都要好几秒。这种"看不见的卡顿",我第一反应就是查 IO。
排查 IO 其实工具不多,麻烦的是容易看错指标。
第一个命令:iostat
iostat -x 1 # -x 看扩展指标,1 秒刷新
输出里 Device 段那些列看着唬人,实际常盯的就四个:
| 指标 | 含义 | 我的判断经验 |
|---|---|---|
%util |
设备处理 IO 的时间占比 | 接近 100% 说明设备饱和,但不等于磁盘坏了 |
await |
单个 IO 请求的平均等待+服务时间(ms) | 机械盘 >20ms 就明显卡;SSD 应该个位数 |
svctm |
设备实际服务时间 | 和 await 差得越多,说明排队越严重 |
avgqu-sz |
平均请求队列长度 | 持续大于 1,说明请求在堆积 |
r/s w/s |
每秒读写请求数(IOPS) | 结合 rkB/s wkB/s 看是大块还是小块 |
最大的一个坑:%util 到 100% 不代表磁盘"满了"。对机械盘大概是饱和了,但对 SSD 和 RAID 阵列(能并行处理多个请求),%util 接近 100% 时可能只是"一直在忙",吞吐还没到上限。这时候要看 await 和 avgqu-sz 一起判断。
反过来,如果 %util 不高但 await 很高,通常是偶尔的慢请求(比如机械盘随机读写、或者阵列里有块盘快坏了在重试)。
谁在疯狂写盘
iostat 只能看到"这块盘很忙",看不到是谁。找进程用:
iotop -o # -o 只显示真正在做 IO 的进程
iotop -oP # 按进程聚合(容器环境更好读)
如果没装 iotop,可以临时用这个土办法:
# 每隔 2 秒看一次哪些进程在写
for p in /proc/[0-9]*; do
pid=${p##*/}
io=$(awk '/^write_bytes/{print $2}' $p/io 2>/dev/null)
[ -n "$io" ] && [ "$io" -gt 1048576 ] && echo "$((io/1048576))MB $pid $(cat $p/comm 2>/dev/null)"
done | sort -rn | head
我见过几次,最后抓出来的元凶都不是业务:一次是 MySQL 的 innodb_log_file_size 太小导致疯狂 checkpoint 刷盘;一次是某个日志忘了配 logrotate,一个文件写了 40G;还有一次是备份脚本和报表任务撞在同一时间跑。
找到进程之后,想知道它到底在读哪些文件:
lsof -p <PID> | grep -v "mem\|cwd\|txt"
# 或者看它当前的 IO 系统调用
strace -p <PID> -e trace=read,write,open
想深挖用 blktrace
iostat 是采样统计,颗粒度不够时用 blktrace 能抓到每一次 IO 的细节:
# 抓 5 秒 /dev/sda 的 IO 事件
blktrace -d /dev/sda -w 5 -o trace
# 解析成人能看的汇总
blkparse -i trace.blktrace.* | head
# 或者直接要统计
btt -i trace.blktrace.*
btt 的输出会把一次 IO 的生命周期拆成几段:
Q2G排队生成请求G2I插入请求队列I2D在队列里等D2C设备真正处理(这段才是磁盘的锅)
看 D2C 和 I2D 的比例:如果 D2C 占大头,是设备慢;如果 I2D 占大头,是上层请求太多、队列调度的问题。这个区分挺关键,不然容易把"应用发太多请求"误判成"磁盘不行"。
优化之前,先测一下真实能力
在说"这盘太慢了"之前,我习惯先跑个 fio 摸底,免得冤枉硬件或者冤枉配置:
# 随机读 4K(模拟 OLTP 数据库负载)
fio -name=randread -filename=/data/testfile -direct=1 -iodepth=32 \
-rw=randread -ioengine=libaio -bs=4k -size=1G -numjobs=1 -runtime=60 -group_reporting
# 顺序写(模拟日志/备份场景)
fio -name=seqwrite -filename=/data/testfile -direct=1 -iodepth=32 \
-rw=write -ioengine=libaio -bs=1M -size=1G -numjobs=1 -runtime=60 -group_reporting
看输出里的 IOPS 和 lat。拿到基准值之后,你就知道现在的 300 IOPS 到底是盘的物理极限,还是配置没调好。
-direct=1别漏,不然测的是页缓存的速度,数据全在内存里,测出来虚高得离谱。
能调的东西
真要改善,常见几个方向,按性价比排:
1. 调度器(机械盘和 SSD 差别大)
cat /sys/block/sda/queue/scheduler # 看当前,方括号里是生效的
# 机械盘:deadline 通常比默认 cfq 好
echo deadline > /sys/block/sda/queue/scheduler
# SSD / NVMe:none 或 noop(别让内核做无谓的排序)
echo none > /sys/block/nvme0n1/queue/scheduler
2. 挂载参数
# 数据库数据盘,关掉 atime(每次读都写一次时间戳,纯浪费)
mount -o remount,noatime,nodiratime /data
改 /etc/fstab 里加 noatime,nodiratime 持久化。这条对读多写少的库收益明显,几乎是白捡的。
3. 数据库侧的刷盘
MySQL 的 innodb_io_capacity 很多人忘了调,默认 200 是按机械盘给的,SSD 上可以往上调(先按 fio 测出来的 IOPS 给个七八成):
innodb_io_capacity = 1000
innodb_io_capacity_max = 2000
4. 该换硬件就换
最后说句实在的:如果 fio 测出来 4K 随机写就 150 IOPS,而业务需要 2000,那调什么参数都白搭,NVMe 换上去比什么都管用。软件调优能解决的是"配置没跟上硬件",解决不了"硬件就不够"。
我一般的排查顺序
iostat -x 1看%util/await/avgqu-sz,确认是不是 IO 问题、是哪块盘iotop -oP定位到进程lsof -p/strace看它在读写什么- 需要的话
blktrace+btt区分"设备慢"还是"请求多" fio测基准,判断是硬件瓶颈还是配置问题- 最后才动调度器 / 挂载参数 / 数据库刷盘配置,改完再
iostat复查
别一上来就改内核参数,那是最后一步。先搞清楚"是谁、在哪、为什么",比调十个参数有用。