Linux运维7 min read次阅读

磁盘 IO 慢的时候,我在看什么

机器看着不忙,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% 时可能只是"一直在忙",吞吐还没到上限。这时候要看 awaitavgqu-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

看输出里的 IOPSlat。拿到基准值之后,你就知道现在的 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 换上去比什么都管用。软件调优能解决的是"配置没跟上硬件",解决不了"硬件就不够"。

我一般的排查顺序

  1. iostat -x 1%util/await/avgqu-sz,确认是不是 IO 问题、是哪块盘
  2. iotop -oP 定位到进程
  3. lsof -p / strace 看它在读写什么
  4. 需要的话 blktrace + btt 区分"设备慢"还是"请求多"
  5. fio 测基准,判断是硬件瓶颈还是配置问题
  6. 最后才动调度器 / 挂载参数 / 数据库刷盘配置,改完再 iostat 复查

别一上来就改内核参数,那是最后一步。先搞清楚"是谁、在哪、为什么",比调十个参数有用。

分享:

评论区