磁盘 IO 飙升导致系统卡死?iostat 与 iotop 深度实战
一、概述
监控大屏上 iowait 突然飙到 80%,SSH 连上去敲个 ls 要等 5 秒才有响应,业务日志疯狂报超时,数据库慢查询告警刷屏。这种场景在 SRE 的日常里出现频率极高,尤其是跑着 MySQL、Elasticsearch、Kafka 这类重 IO 业务的机器上。CPU 看着不高,内存也没爆,但系统就是卡得像被冻住了一样——十有八九是磁盘 IO 出了问题。
磁盘 IO 问题的棘手之处在于,它不像 CPU 打满那样一眼就能看出来。IO 瓶颈可能藏在文件系统层、Block 层、设备驱动层,甚至是 RAID 卡的缓存策略里。一个 %util 100% 到底意味着什么?await 高是磁盘慢还是队列深?svctm 这个指标还能不能信?这些问题如果搞不清楚,排查就会走弯路。
这篇文章从 Linux IO 栈的全貌讲起,把 iostat、iotop、blktrace、fio、bpftrace 这套工具链串起来,覆盖从"发现 IO 问题"到"定位根因"再到"调优解决"的完整链路。
全栈视角:从 VFS 到块设备驱动,逐层拆解 IO 路径,不停留在工具表面。
工具链完整:iostat 看全局、iotop 定进程、blktrace 追请求、fio 做基准、bpftrace 抓延迟。
面向实战:每个工具都给出真实场景下的输出解读,不是照搬 man page。
调优闭环:不只是发现问题,还覆盖 readahead、dirty ratio、调度器等内核参数调优。
场景一:生产环境突发 IO 飙升,系统响应变慢甚至卡死,需要快速定位是哪个进程在疯狂读写。
场景二:数据库服务器 IO 延迟周期性升高,需要区分是磁盘性能不足还是应用层 IO 模式有问题。
场景三:新机器上线前需要做磁盘性能基准测试,评估 SSD/HDD 是否满足业务 IOPS 和吞吐需求。
场景四:容器环境下多个 Pod 共享磁盘,需要找出 IO 资源的争抢源头。
二、Linux IO 栈全景与调度器
要排查 IO 问题,首先得搞清楚一个 IO 请求从应用程序发出到磁盘完成,中间经过了哪些环节。不理解 IO 栈的层次结构,看 iostat 的输出就只是在看数字。
应用程序 (read/write/pread/pwrite/io_uring)|vVFS (Virtual File System) --- Page Cache|v文件系统 (ext4 / xfs / btrfs)|vBlock Layer (通用块层)| - IO 合并 (merge)| - IO 调度 (mq-deadline/bfq/kyber/none)| - 请求队列 (多队列 blk-mq)v设备驱动 (NVMe driver / SCSI / virtio-blk)|v物理设备 (NVMe SSD / SATA SSD / HDD / 云盘)
每一层都可能成为瓶颈,排查时需要逐层排除:
VFS + Page Cache 层:大部分读请求会命中 Page Cache 直接返回,根本不会到磁盘。如果 free -h 看到 buff/cache 很小,或者 sar -B 显示 pgpgin 很高,说明缓存命中率低,大量读请求穿透到了磁盘。写请求默认走 writeback 模式,先写到 Page Cache 的脏页里,由内核的 pdflush/flush 线程异步刷盘。
文件系统层:ext4 的 journal 写入、xfs 的 log 写入都会产生额外的 IO。文件系统碎片化严重时,顺序读会退化成随机读。filefrag 命令可以查看文件碎片程度。
Block Layer:这是 iostat 能观测到的层。IO 请求在这里被合并(相邻的小 IO 合并成大 IO)、排序、调度。blk-mq(多队列块层)是 6.x 内核的默认架构,每个 CPU 核心有独立的软件队列,减少了锁竞争。
设备驱动和物理设备:NVMe 设备有自己的硬件多队列(通常 64 个队列,每队列 64K 深度),SATA SSD 只有单队列(NCQ 深度 32)。这个差异直接影响并发 IO 性能。
内核 6.x 提供了四种 IO 调度器,针对不同设备类型选择合适的调度器对性能影响很大。
# 查看当前磁盘使用的调度器(方括号标记的是当前生效的)cat /sys/block/sda/queue/scheduler# 输出示例:[mq-deadline] kyber bfq none# 运行时切换调度器(立即生效,不需要重启)echo "bfq" > /sys/block/sda/queue/scheduler
四种调度器的选型策略:
| none | |||
| mq-deadline | |||
| bfq | |||
| kyber |
选型建议:NVMe SSD 直接用 none,不要画蛇添足加调度器。HDD 用 mq-deadline 保证读延迟可控。多用户共享 HDD 的场景(比如编译服务器)用 bfq 防止某个进程独占 IO 带宽。kyber 在实际生产中用得不多,它的自适应机制在负载波动大的场景下表现不够稳定。
# 持久化调度器配置(udev 规则)cat > /etc/udev/rules.d/60-io-scheduler.rules << 'EOF'# NVMe SSD 使用 noneACTION=="add|change", KERNEL=="nvme[0-9]*", ATTR{queue/scheduler}="none"# SATA SSD 使用 mq-deadlineACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"# HDD 使用 mq-deadlineACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"EOF# 重新加载 udev 规则udevadm control --reload-rules && udevadm trigger
理解 SSD 和 HDD 的物理特性差异,是正确解读 IO 指标的前提。
%util |
这里有一个非常关键的认知:%util 对 SSD 几乎没有参考价值。HDD 是单通道设备,%util 100% 确实意味着磁盘忙不过来。但 SSD 内部有大量并行通道,%util 100% 可能只用了实际能力的 10%。判断 SSD 是否到达瓶颈,应该看 await(IO 延迟)和实际 IOPS 是否接近设备标称值。
三、iostat 输出深度解读
iostat 是 IO 排查的第一站,但它的输出字段很多,不少人只会看 %util,这远远不够。
# 最常用的命令:每秒刷新一次,显示扩展信息,单位用 MB# -x 扩展模式 -m 单位MB -t 显示时间戳 1 间隔1秒iostat -xmt 1# 只看特定磁盘iostat -xmt -d nvme0n1 sda 1# 看第一次输出要注意:第一行是系统启动以来的累计平均值,不是实时数据# 从第二行开始才是每秒的实时数据,排查问题时忽略第一行
一条典型的 iostat -x 输出:
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz d/s dkB/s drqm/s %drqm d_await dareq-sz f/s f_await aqu-sz %utilsda 850.00 13600.0 45.00 5.03 0.85 16.00 320.00 25600.0 120.00 27.27 2.30 80.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 1.25 78.40
核心指标解读:
r/sw/s | ||
rkB/swkB/s | ||
rrqm/swrqm/s | ||
r_awaitw_await | 最重要的延迟指标 | |
aqu-sz | ||
%util | ||
rareq-szwareq-sz |
已废弃的 svctm:老版本 iostat 有 svctm(设备服务时间)字段,sysstat 12.x 已经标记为不可靠并计划移除。这个值是用 %util / (r/s + w/s) 反算出来的,在多队列设备上完全失真。不要再用 svctm 做任何判断。
# 场景判断速查:# 1. await 高 + aqu-sz 高 + %util 高 → 磁盘确实忙不过来(HDD 常见)# 2. await 高 + aqu-sz 低 + %util 低 → 单个 IO 慢,可能是磁盘硬件问题或 RAID 降级# 3. await 正常 + w/s 极高 + wkB/s 低 → 大量小写入,考虑合并 IO 或调大 dirty ratio# 4. rrqm/s 接近 0 + rareq-sz 很小 → 纯随机读,Page Cache 没起作用# 5. w_await 远高于 r_await → 写入瓶颈,检查 fsync 频率和 journal 模式
iostat 只能看实时数据,要回溯历史得靠 sar。sysstat 默认每 10 分钟采集一次数据,保存在 /var/log/sysstat/ 或 /var/log/sa/ 下。
# 查看今天的磁盘 IO 历史(-d 磁盘统计,-p 显示设备名)sar -dp 0# 查看昨天的数据sar -dp -f /var/log/sysstat/sa$(date -d yesterday +%d)# 查看指定时间段sar -dp -s 02:00:00 -e 04:00:00# 输出示例:# 02:10:01 DEV tps rkB/s wkB/s areq-sz aqu-sz await %util# 02:20:01 sda 1250.00 10000.00 40000.00 40.00 3.50 2.80 92.00# 02:30:01 sda 120.00 960.00 2400.00 28.00 0.15 1.25 12.00
凌晨 2:10 到 2:20 之间 IO 明显飙升,tps 从 120 跳到 1250,%util 从 12% 到 92%。结合业务日志看这个时间段在做什么——大概率是定时任务(备份、日志轮转、ETL 作业)。
四、iotop 定位 IO 密集进程
iostat 告诉你磁盘整体很忙,但不告诉你是谁在读写。这时候需要 iotop 来定位到具体进程。
# 需要 root 权限(依赖内核的 taskstats 接口)# -o 只显示有 IO 活动的进程(过滤掉空闲的)# -P 显示进程而不是线程# -a 累积模式(显示自启动以来的累计 IO 量)sudo iotop -oP# 推荐使用 iotop-c(C 语言重写版,性能更好)# Ubuntu: sudo apt install iotop-csudo iotop-c -oP
Total DISK READ: 125.50 M/s | Total DISK WRITE: 42.30 M/sActual DISK READ: 125.50 M/s | Actual DISK WRITE: 8.75 M/sTID PRIO USER DISK READ DISK WRITE SWAPIN IO> COMMAND12847 be/4 mysql 98.20 M/s 5.60 M/s 0.00 % 82.35 % mysqld --defaults-file=/etc/mysql/my.cnf3021 be/4 root 22.10 M/s 0.00 B/s 0.00 % 15.20 % tar czf /backup/db-20260206.tar.gz /var/lib/mysql891 be/4 elastic 5.20 M/s 36.70 M/s 0.00 % 8.50 % java -Xms16g ... elasticsearch
几个关键信息:
Total DISK READ/WRITE:所有进程请求的 IO 总量(经过 Page Cache 之前)。
Actual DISK READ/WRITE:实际落到磁盘的 IO 量。Actual WRITE 远小于 Total WRITE 是正常的,因为写入先到 Page Cache,异步刷盘。
**IO>**:该进程等待 IO 的时间占比,这个值高说明进程被 IO 阻塞了。
PRIO:IO 优先级,be/4 表示 best-effort 类第 4 级(默认值)。
上面的输出一眼就能看出:mysql 进程在疯狂读数据(98 MB/s),同时有个 tar 备份任务也在读(22 MB/s)。两个大读取任务叠加,磁盘带宽被打满了。
找到了捣乱的进程,如果不能直接 kill,可以用 ionice 降低它的 IO 优先级:
# IO 调度类:# 1 = Realtime(实时,慎用)# 2 = Best-effort(默认,0-7 级,数字越小优先级越高)# 3 = Idle(空闲时才执行,适合备份任务)# 把备份进程降到 Idle 级别(只在磁盘空闲时才给它 IO)sudo ionice -c 3 -p 3021# 启动备份任务时直接指定低优先级sudo ionice -c 3 nice -n 19 tar czf /backup/db-20260206.tar.gz /var/lib/mysql# 注意:ionice 只在 bfq 和 mq-deadline 调度器下生效# none 调度器(NVMe 默认)不支持 IO 优先级
iotop 是交互式的,不方便脚本化采集。pidstat 可以按固定间隔输出进程 IO 数据:
# -d 显示 IO 统计 1 每秒采集 10 采集10次pidstat -d 1 10# 只看特定进程pidstat -d -p 12847 1# 输出示例:# Time UID PID kB_rd/s kB_wr/s kB_ccwr/s iodelay Command# 15:30:01 999 12847 98200.00 5600.00 800.00 45 mysqld
kB_ccwr/s 是被取消的写入量(写入 Page Cache 后又被覆盖,没有实际落盘),iodelay 是进程因为 IO 等待而被延迟的 tick 数。
五、blktrace + btt 深度分析
iostat 和 iotop 能解决 80% 的 IO 问题,但遇到复杂场景——比如需要知道 IO 请求在块层各个阶段分别花了多少时间——就需要 blktrace 出场了。
blktrace 在内核的块层埋了追踪点,记录每个 IO 请求的完整生命周期:
Q (Queued) → 请求进入块层队列G (Get request) → 分配 request 结构体M (Merged) → 与已有请求合并I (Inserted) → 插入调度器队列D (Dispatched) → 下发到设备驱动C (Completed) → 设备完成 IO
每个阶段之间的时间差就是该阶段的耗时。Q→C 是总耗时,D→C 是设备实际服务时间,Q→D 是在软件层排队和调度的时间。
# 采集 sda 的块层追踪数据,持续 10 秒sudo blktrace -d /dev/sda -w 10 -o trace# 会生成 trace.blktrace.0, trace.blktrace.1 ... 每个 CPU 一个文件# 用 blkparse 解析成可读格式blkparse -i trace -o trace.txt# 输出示例(每行一个事件):# 8,0 1 1 0.000000000 12847 Q R 123456 + 8 [mysqld]# 8,0 1 2 0.000001200 12847 G R 123456 + 8 [mysqld]# 8,0 1 3 0.000002500 12847 I R 123456 + 8 [mysqld]# 8,0 1 4 0.000015000 12847 D R 123456 + 8 [mysqld]# 8,0 1 5 0.000850000 0 C R 123456 + 8 [0]# 解读:mysqld 发起了一个读请求,扇区 123456 开始读 8 个扇区(4KB)# Q→D 排队 15us,D→C 设备服务 835us,总耗时 850us
逐行看 blkparse 输出不现实,btt 工具可以自动统计各阶段的延迟分布:
# 用 btt 分析(需要先用 blkparse 生成二进制格式)blkparse -i trace -d trace.binbtt -i trace.bin# btt 输出的关键段落:# ==================== All Devices ====================# ALL MIN AVG MAX N# --------------- ------------- ------------- ------------- -----------# Q2C 0.000085000 0.001250000 0.025000000 12847# Q2D 0.000005000 0.000018000 0.000350000 12847# D2C 0.000080000 0.001232000 0.024800000 12847# Q2C = 总延迟(队列到完成)# Q2D = 软件层延迟(队列到下发)# D2C = 硬件层延迟(下发到完成)
上面的数据说明:平均总延迟 1.25ms,其中软件层只占 0.018ms,硬件层占 1.232ms。瓶颈在设备本身,不在内核调度。如果反过来 Q2D 很大而 D2C 很小,说明是调度器或队列配置有问题。
blktrace 的数据还可以用 iowatcher 生成可视化图表,直观展示 IO 模式:
# 安装 iowatchersudo apt install iowatcher# 生成 SVG 图表iowatcher -t trace -o io-pattern.svg# 图表包含:IOPS 时间线、吞吐时间线、IO 延迟分布、IO 偏移量分布(可以看出是顺序还是随机)
六、fio 磁盘性能基准测试
排查 IO 问题时经常需要回答一个基本问题:这块磁盘到底能跑多快?是磁盘本身性能不行,还是应用的 IO 模式有问题?fio 是回答这个问题的标准工具。
# 安装 fiosudo apt install fio # Debian/Ubuntusudo dnf install fio # RHEL/Fedora
fio 的参数很多,但核心就这几个:
--rw | readwrite/randread/randwrite/randrw | |
--bs | 4k128k/1m(顺序IO) | |
--iodepth | ||
--ioengine | libaioio_uring(推荐,内核 6.x) | |
--numjobs | ||
--size | ||
--direct | 1 | |
--runtime |
# 场景1:随机读 IOPS(模拟数据库查询)fio --name=rand-read --ioengine=io_uring --rw=randread \--bs=4k --iodepth=64 --numjobs=4 --size=4G \--direct=1 --runtime=60 --group_reporting \--filename=/dev/nvme0n1 # 注意:直接测裸设备会销毁数据!测试用文件更安全# 更安全的方式:在文件系统上测试fio --name=rand-read --ioengine=io_uring --rw=randread \--bs=4k --iodepth=64 --numjobs=4 --size=4G \--direct=1 --runtime=60 --group_reporting \--directory=/mnt/test# 场景2:随机写 IOPS(模拟数据库写入)fio --name=rand-write --ioengine=io_uring --rw=randwrite \--bs=4k --iodepth=64 --numjobs=4 --size=4G \--direct=1 --runtime=60 --group_reporting \--directory=/mnt/test# 场景3:顺序读吞吐(模拟大文件扫描、备份)fio --name=seq-read --ioengine=io_uring --rw=read \--bs=1m --iodepth=16 --numjobs=1 --size=8G \--direct=1 --runtime=60 --group_reporting \--directory=/mnt/test# 场景4:混合随机读写 7:3(模拟 OLTP 数据库)fio --name=mixed-rw --ioengine=io_uring --rw=randrw --rwmixread=70 \--bs=4k --iodepth=32 --numjobs=4 --size=4G \--direct=1 --runtime=60 --group_reporting \--directory=/mnt/test
rand-read: (groupid=0, jobs=4): err= 0: pid=5678read: IOPS=185.2k, BW=723MiB/s (758MB/s)slat (nsec): min=1200, max=85000, avg=2850.00, stdev=1200.00clat (usec): min=45, max=12500, avg=1350.00, stdev=680.00lat (usec): min=48, max=12520, avg=1353.00, stdev=681.00clat percentiles (usec):| 1.00th=[ 120], 5.00th=[ 245], 10.00th=[ 400],| 50.00th=[ 1150], 90.00th=[ 2350], 95.00th=[ 2900],| 99.00th=[ 4500], 99.50th=[ 5800], 99.99th=[10800]bw ( KiB/s): min=680000, max=760000, per=100.00%, avg=740800.00iops : min=170000, max=190000, avg=185200.00
关键指标:
IOPS=185.2k:每秒 18.5 万次随机读,这是一块性能不错的 NVMe SSD。
slat(submission latency):提交延迟,从应用发起到进入内核,通常在微秒级。
clat(completion latency):完成延迟,从进入内核到 IO 完成,这是最关注的指标。
lat:总延迟 = slat + clat。
clat percentiles:延迟分位数,P99=4.5ms 说明 99% 的请求在 4.5ms 内完成。关注 P99 和 P99.9,平均值会掩盖长尾延迟。
内核 6.x 环境下强烈推荐使用 io_uring 引擎替代传统的 libaio:
# 对比测试:同样参数,只换引擎# libaiofio --name=aio-test --ioengine=libaio --rw=randread \--bs=4k --iodepth=128 --numjobs=1 --size=4G \--direct=1 --runtime=30 --directory=/mnt/test# io_uringfio --name=uring-test --ioengine=io_uring --rw=randread \--bs=4k --iodepth=128 --numjobs=1 --size=4G \--direct=1 --runtime=30 --directory=/mnt/test
io_uring 的优势在于减少了系统调用次数(通过共享内存的提交/完成队列),在高 IOPS 场景下能比 libaio 高出 10%-30% 的性能。现代数据库(如 PostgreSQL 16+、RocksDB)已经原生支持 io_uring。
七、文件系统选择
文件系统是 IO 栈中直接影响性能的一层,选错文件系统可能导致性能差距达到 2-3 倍。
选型建议:
数据库服务器(MySQL/PostgreSQL):xfs。延迟分配和优秀的并发写入性能对数据库友好,RHEL 默认文件系统。
通用 Linux 服务器:ext4。最稳定、最成熟、调优资料最多,Ubuntu 默认文件系统。
需要快照/压缩的场景(日志存储、容器存储):btrfs。透明压缩可以节省 30%-50% 的磁盘空间,快照功能方便备份。
Kubernetes 节点:xfs。containerd/CRI-O 的 overlayfs 在 xfs 上表现更好。
# ext4 高性能挂载参数mount -o noatime,nodiratime,barrier=0,data=writeback /dev/sda1 /data# noatime: 不更新访问时间戳,减少写入# nodiratime: 不更新目录访问时间# barrier=0: 关闭写屏障(仅在有 BBU 的 RAID 卡上使用,否则断电丢数据)# data=writeback: 元数据日志模式,比默认的 ordered 快但断电风险略高# xfs 高性能挂载参数mount -o noatime,logbufs=8,logbsize=256k /dev/sda1 /data# logbufs=8: 增大日志缓冲区数量# logbsize=256k: 增大日志缓冲区大小# /etc/fstab 持久化配置/dev/nvme0n1p1 /data xfs defaults,noatime,logbufs=8,logbsize=256k 0 2
八、IO 性能调优
预读是内核在检测到顺序读模式时,提前读取后续数据到 Page Cache 的机制。对顺序读密集的场景(日志分析、数据导出)效果显著。
# 查看当前预读值(单位:512字节扇区,默认 256 = 128KB)blockdev --getra /dev/sda# 调大预读值(适合顺序读场景,如 Kafka、HDFS)sudo blockdev --setra 2048 /dev/sda # 1MB 预读# 调小预读值(适合纯随机读场景,如数据库 OLTP)sudo blockdev --setra 64 /dev/sda # 32KB 预读# 持久化配置(udev 规则)echo 'ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{bdi/read_ahead_kb}="1024"' \> /etc/udev/rules.d/61-readahead.rules
脏页参数控制 Page Cache 中脏数据的刷盘策略,直接影响写入性能和数据安全性。
# 查看当前脏页参数sysctl vm.dirty_ratio vm.dirty_background_ratio vm.dirty_expire_centisecs vm.dirty_writeback_centisecs# 参数说明:# vm.dirty_ratio = 20 # 脏页占内存 20% 时,写入进程被阻塞,同步刷盘(硬上限)# vm.dirty_background_ratio = 10 # 脏页占内存 10% 时,后台线程开始异步刷盘(软上限)# vm.dirty_expire_centisecs = 3000 # 脏页超过 30 秒必须刷盘# vm.dirty_writeback_centisecs = 500 # 后台刷盘线程每 5 秒唤醒一次
不同场景的调优策略:
# 数据库服务器(低延迟优先,减少突发刷盘导致的延迟毛刺)sysctl -w vm.dirty_ratio=5sysctl -w vm.dirty_background_ratio=2sysctl -w vm.dirty_expire_centisecs=1000sysctl -w vm.dirty_writeback_centisecs=100# 日志/流式写入服务器(吞吐优先,允许更多脏页缓冲)sysctl -w vm.dirty_ratio=40sysctl -w vm.dirty_background_ratio=20sysctl -w vm.dirty_expire_centisecs=6000sysctl -w vm.dirty_writeback_centisecs=500# 持久化到 /etc/sysctl.d/60-io-tuning.confcat > /etc/sysctl.d/60-io-tuning.conf << 'EOF'vm.dirty_ratio = 5vm.dirty_background_ratio = 2vm.dirty_expire_centisecs = 1000vm.dirty_writeback_centisecs = 100EOFsysctl --system
# 查看块设备队列深度cat /sys/block/sda/queue/nr_requests# 默认值通常是 256# NVMe 设备的硬件队列深度cat /sys/block/nvme0n1/queue/nr_requests# 对于高并发 IO 场景,可以适当增大队列深度echo 1024 > /sys/block/sda/queue/nr_requests# 查看当前 IO 合并策略cat /sys/block/sda/queue/nomerges# 0 = 允许合并(默认) 1 = 禁止前向合并 2 = 完全禁止合并# 纯随机 IO 场景可以设为 2,省去合并检查的开销
在多租户或容器环境下,用 cgroup v2 的 io 控制器限制进程的 IO 带宽和 IOPS:
# 查看设备号(major:minor)ls -l /dev/sda# brw-rw---- 1 root disk 8, 0 ... → 设备号 8:0# 创建 cgroup 并设置 IO 限制mkdir -p /sys/fs/cgroup/backup-jobsecho"+io" > /sys/fs/cgroup/backup-jobs/cgroup.subtree_control# 限制 sda 上的读写带宽为 50MB/s,IOPS 为 1000echo"8:0 rbps=52428800 wbps=52428800 riops=1000 wiops=1000" \> /sys/fs/cgroup/backup-jobs/io.max# 把备份进程加入这个 cgroupecho$BACKUP_PID > /sys/fs/cgroup/backup-jobs/cgroup.procs# Kubernetes 中通过 Pod 的 resources 或 annotation 配置# 也可以用 io.latency 控制器设置延迟目标echo"8:0 target=5000" > /sys/fs/cgroup/db-workload/io.latency# 保证 db-workload 组的 IO 延迟不超过 5ms,其他组让路
九、常见 IO 问题排查案例
现象:每天凌晨 2:00-2:30,MySQL 慢查询数量暴增 10 倍,await 从 0.5ms 飙到 15ms。
排查过程:
# 1. 先看 iostat 确认 IO 确实有问题iostat -xmt 1# 发现 w_await 从 0.5ms 涨到 15ms,wkB/s 从 20MB/s 涨到 180MB/s# 2. iotop 找出谁在写sudo iotop -oP# 发现两个大户:# mysqld → 20MB/s 写入(正常业务)# rsync → 160MB/s 读取(备份任务在全量拷贝数据目录)# 3. 确认 rsync 是定时任务触发的ps aux | grep rsync# root 5432 rsync -avz /var/lib/mysql/ backup-server:/backup/mysql/# 4. rsync 的大量顺序读把磁盘带宽吃满,MySQL 的随机 IO 被挤压
解决方案:
# 方案1:用 ionice 降低备份任务优先级ionice -c 3 nice -n 19 rsync -avz /var/lib/mysql/ backup-server:/backup/mysql/# 方案2:用 cgroup v2 限制备份任务的 IO 带宽echo "8:0 rbps=52428800 wbps=52428800" > /sys/fs/cgroup/backup/io.max# 方案3:用 rsync 的 --bwlimit 限制传输速率rsync -avz --bwlimit=50000 /var/lib/mysql/ backup-server:/backup/mysql/# --bwlimit 单位是 KB/s,50000 = 50MB/s
现象:一台跑 Elasticsearch 的机器,写入吞吐突然从 200MB/s 降到 40MB/s,iostat 显示 w/s 很高但 wareq-sz 只有 4KB。
排查过程:
# 1. iostat 看写入模式iostat -xmt -d sda 1# w/s=8500 wkB/s=34000 wareq-sz=4.0 w_await=3.5 %util=98# 大量 4KB 小写入,这不像 ES 的正常行为(ES 的 segment merge 是大块顺序写)# 2. 用 blktrace 看写入来源sudo blktrace -d /dev/sda -w 5 -o journal-traceblkparse -i journal-trace | grep "W" | awk '{print $NF}' | sort | uniq -c | sort -rn | head# 发现大量写入来自 [jbd2/sda1-8],这是 ext4 的 journal 线程# 3. 检查文件系统 journal 模式tune2fs -l /dev/sda1 | grep "Journal"# Default mount options: journal_data# journal_data 模式会把所有数据都写一遍 journal,写放大 2 倍!
解决方案:
# 切换到 ordered 模式(只对元数据做 journal,数据直接写)sudo mount -o remount,data=ordered /data# 或者在 /etc/fstab 中修改/dev/sda1 /data ext4 defaults,noatime,data=ordered 0 2# 如果是 ES 这种自己管理数据一致性的应用,甚至可以用 writeback 模式# 但要确保有 UPS 或 BBU,否则断电可能丢数据
现象:监控告警 NVMe 磁盘 %util 持续 100%,但业务没有感知到任何延迟。
排查过程:
# 1. iostat 看详细指标iostat -xmt -d nvme0n1 1# r/s=45000 rkB/s=180000 r_await=0.08 aqu-sz=3.6 %util=100# r_await 只有 0.08ms(80 微秒),完全正常# IOPS 45K,这块 NVMe 标称能跑 500K IOPS,远没到极限# 2. 这是 %util 计算方式的问题# %util = (IO 请求数 * 每次 IO 耗时) / 采样间隔# 当并发 IO 足够多时,即使每个 IO 很快,%util 也会算到 100%# 对于多队列设备,%util 100% 不代表设备饱和
结论:这是一个误告警。对 NVMe SSD 应该基于 await 和 IOPS 来判断是否饱和,而不是 %util。监控告警规则需要调整:
# Prometheus 告警规则(修正版)# 不再用 %util 告警 NVMe 设备,改用 await-alert:DiskIOHighLatencyexpr:|rate(node_disk_io_time_weighted_seconds_total[5m])/ rate(node_disk_io_time_seconds_total[5m]) > 0.005for:5mlabels:severity:warningannotations:summary:"磁盘 IO 平均延迟超过 5ms"
十、bpftrace 追踪 IO 延迟
iostat 给的是平均值,blktrace 数据量太大不适合长期运行。bpftrace 可以用极低的开销实时追踪 IO 延迟分布,是定位长尾延迟问题的利器。
# bcc-tools 自带的 biolatency 脚本(最简单的用法)sudo biolatency-bpfcc -D 1# -D 按磁盘分别统计 1 每秒输出一次# 输出示例:# disk = nvme0n1# usecs : count distribution# 0 -> 1 : 0 | |# 2 -> 3 : 0 | |# 4 -> 7 : 125 |** |# 8 -> 15 : 2840 |****************************************|# 16 -> 31 : 1950 |*************************** |# 32 -> 63 : 680 |********* |# 64 -> 127 : 245 |*** |# 128 -> 255 : 42 | |# 256 -> 511 : 8 | |# 512 -> 1023 : 3 | |# 1024 -> 2047 : 1 | |# 大部分 IO 在 8-31 微秒完成,但有少量请求到了毫秒级——这就是长尾延迟
biolatency 只能看全局分布,如果要按进程区分,需要自己写 bpftrace 脚本:
# 保存为 io-latency-by-process.btsudo bpftrace -e 'tracepoint:block:block_rq_issue{@start[args->dev, args->sector] = nsecs;@comm[args->dev, args->sector] = comm;}tracepoint:block:block_rq_complete/@start[args->dev, args->sector]/{$lat = (nsecs - @start[args->dev, args->sector]) / 1000; // 转换为微秒@latency[@comm[args->dev, args->sector]] = hist($lat);delete(@start[args->dev, args->sector]);delete(@comm[args->dev, args->sector]);}END{clear(@start);clear(@comm);}'# 输出会按进程名分别显示延迟直方图:# @latency[mysqld]:# [8, 16) 1250 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|# [16, 32) 680 |@@@@@@@@@@@@@@@@@@@@@ |# ...# @latency[rsync]:# [128, 256) 420 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|# [256, 512) 380 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |# ...
当需要看每一条 IO 请求的详细信息时,用 biosnoop:
# 追踪所有 IO 请求,显示进程名、延迟、扇区等sudo biosnoop-bpfcc -d nvme0n1# 输出示例:# TIME(s) COMM PID DISK T SECTOR BYTES LAT(ms)# 0.000 mysqld 12847 nvme0n1 R 12345678 4096 0.08# 0.001 mysqld 12847 nvme0n1 R 12345686 4096 0.09# 0.003 jbd2/sda1-8 891 nvme0n1 W 98765432 16384 0.15# 0.850 rsync 5432 nvme0n1 R 55555555 131072 0.12# 只看延迟超过 1ms 的慢 IOsudo biosnoop-bpfcc -d nvme0n1 -Q | awk '$NF > 1.0'
有时候块设备层延迟正常,但文件系统层有额外开销(锁竞争、journal 等待)。bcc-tools 提供了文件系统级别的慢操作追踪:
# 追踪 ext4 上超过 1ms 的操作sudo ext4slower-bpfcc 1# 追踪 xfs 上超过 1ms 的操作sudo xfsslower-bpfcc 1# 输出示例:# TIME COMM PID T BYTES OFF_KB LAT(ms) FILENAME# 15:30:01 mysqld 12847 R 16384 1024 2.35 ibdata1# 15:30:01 mysqld 12847 S 0 0 5.80 ib_logfile0# T 列:R=read W=write O=open S=fsync# fsync 延迟 5.8ms,这可能是 journal 写入导致的
这个工具能直接关联到文件名,比 biosnoop 更容易定位到具体是哪个文件的 IO 有问题。
十一、IO 排查流程总结
系统卡顿/业务超时|vtop 看 %wa (iowait) ──── 低 → 不是 IO 问题,排查 CPU/内存/网络|高viostat -xmt 1 ──── await 正常 → 可能是应用层阻塞,不是磁盘瓶颈|await 高v判断设备类型 ──── NVMe → 看 await + IOPS,忽略 %util| HDD → %util + await + aqu-sz 综合判断viotop -oP ──── 找到 IO 大户进程|v分析 IO 模式 ──── wareq-sz/rareq-sz 小(4-8KB) → 随机 IO| wareq-sz/rareq-sz 大(128K+) → 顺序 IOv深入分析 ──── blktrace+btt 看各阶段延迟| bpftrace 看延迟分布和长尾| fio 做基准对比v调优/解决 ──── 调度器/readahead/dirty ratio/cgroup 限制/硬件升级
top | top -d 1 | ||
iostat | iostat -xmt 1 | ||
sar | sar -dp -s 02:00 -e 04:00 | ||
iotop | sudo iotop -oP | ||
pidstat | pidstat -d 1 10 | ||
ionice | ionice -c 3 -p <PID> | ||
blktrace | blktrace -d /dev/sda -w 10 | ||
btt | btt -i trace.bin | ||
fio | |||
biolatency | sudo biolatency-bpfcc -D 1 | ||
biosnoop | sudo biosnoop-bpfcc -d sda | ||
ext4slower | sudo ext4slower-bpfcc 1 |
/sys/block/*/queue/scheduler | none,HDD: mq-deadline | ||
blockdev --setra | |||
vm.dirty_ratio | |||
vm.dirty_background_ratio | |||
vm.dirty_expire_centisecs | |||
/sys/block/*/queue/nr_requests | |||
/sys/block/*/queue/nomerges | |||
noatime |
十二、总结
1)IO 栈认知层面
分层排查是基本功:一个 IO 请求从 VFS 到文件系统、Block Layer、设备驱动、物理设备,至少经过五层。排查 IO 问题的核心方法论就是逐层定位,而不是上来就调内核参数碰运气。搞清楚瓶颈在软件层(Q2D)还是硬件层(D2C),后续的调优方向完全不同。
%util 的适用边界必须搞清楚:这是生产环境中最高频的认知误区。HDD 是单通道设备,%util 100% 确实意味着饱和;但 SSD 内部有大量并行通道,%util 100% 可能只用了实际能力的 10%。NVMe 设备判断是否饱和,看 await 和实际 IOPS 与标称值的差距,%util 直接忽略。
await 是延迟排查的锚点:它包含队列等待和设备服务两部分时间。HDD 正常 5-15ms,SATA SSD 正常 0.05-0.5ms,NVMe 正常 0.01-0.1ms。超出正常范围就要往下查,低于正常范围说明磁盘还有余量。
svctm 已废弃,不要再用:sysstat 12.x 明确标记该字段不可靠,它是用 %util 反算出来的,在 blk-mq 多队列架构下完全失真。
2)工具链层面
iostat → iotop → blktrace → bpftrace,四级递进:iostat 回答"磁盘整体忙不忙",iotop 回答"谁在读写",blktrace 回答"IO 请求在各阶段花了多少时间",bpftrace 回答"延迟分布长什么样、长尾在哪里"。每个工具解决一个层面的问题,不要指望一个工具搞定所有事。
fio 基准测试是调优的前提:不做基准就调参数等于盲调。先用 fio 的 io_uring 引擎跑出磁盘在 4K 随机读写、128K 顺序读写下的 IOPS 和吞吐上限,再拿业务实际负载的 iostat 数据去对比,才能判断是磁盘能力不足还是应用 IO 模式有问题。
3)调优参数层面
IO 调度器选型直接影响性能:NVMe 用 none(硬件自带调度,软件层不要画蛇添足),HDD 用 mq-deadline(保证读延迟可控),多租户共享 HDD 用 bfq(按进程公平分配带宽)。用 udev 规则持久化,不要每次重启后手动设置。
脏页参数是写入延迟毛刺的主因:数据库场景把 dirty_ratio 调到 5%、dirty_background_ratio 调到 2%,可以显著减少突发刷盘导致的延迟抖动。这是最常被忽略但效果最明显的调优项。
readahead 要按场景区分:Kafka、HDFS 这类顺序读密集的场景调大到 1MB 以上;MySQL OLTP 这类纯随机读场景调小到 32KB,避免预读浪费带宽。
cgroup v2 的 io 控制器是多租户环境的刚需:io.max 做硬限制、io.latency 做延迟保障、io.weight 做权重分配,三者配合使用可以在不升级硬件的前提下解决 IO 争抢问题。
1)eBPF/bpftrace 存储观测体系
本文用到的 biolatency、biosnoop 只是 bcc-tools 的预置脚本,实际生产中经常需要自定义追踪逻辑。bpftrace 支持挂载到 block_rq_issue、block_rq_complete 等 tracepoint 上,按进程、按设备、按 IO 大小做多维度延迟分布统计。更进一步,可以用 libbpf + CO-RE 编写常驻的 IO 观测 daemon,替代 blktrace 实现低开销的长期追踪。Brendan Gregg 的 bpftrace 工具集和 libbpf-bootstrap 项目是两个值得深入研究的起点。
2)io_uring 异步 IO 框架
io_uring 通过用户态和内核态共享的 SQ(提交队列)/CQ(完成队列)环形缓冲区,将系统调用开销降到接近零。在高 IOPS 场景下比 libaio 高出 10%-30% 的性能。值得关注的高级特性包括:固定缓冲区(fixed buffers)避免每次 IO 的内存注册开销、链式提交(linked SQEs)实现原子性多步操作、多环共享(IORING_SETUP_ATTACH_WQ)减少内核线程数。PostgreSQL 16+、RocksDB、SPDK 已经原生集成 io_uring,理解其工作机制对评估和调优这些系统的 IO 性能有直接帮助。
3)NVMe 深度优化
NVMe 设备的性能调优远不止选个 none 调度器。硬件多队列到 CPU 核心的映射关系(通过 /proc/interrupts 和 smp_affinity 查看和调整)在 NUMA 架构下对延迟影响显著——跨 NUMA 节点访问 NVMe 队列会增加 30%-50% 的延迟。此外,NVMe 的 namespace 管理、SR-IOV 虚拟化直通、CMB(Controller Memory Buffer)、ZNS(Zoned Namespace)等特性在大规模存储集群中逐渐落地,nvme-cli 工具集是管理和诊断 NVMe 设备的必备技能。
4)存储栈全链路可观测性建设
把 iostat/sar 的指标接入 Prometheus(通过 node_exporter 的 diskstats collector),用 Grafana 构建磁盘 IO 大盘,配合本文提到的告警规则(基于 await 而非 %util),形成从指标采集、异常告警到根因定位的完整闭环。对于 Kubernetes 环境,还需要关注 CSI 驱动层面的 IO 指标和 PV/PVC 级别的 IO 隔离。
参考资料
Linux Block IO Layer - 内核官方块层文档,理解 blk-mq 架构的权威来源
iostat(1) man page - sysstat - iostat 各字段的精确定义,排查时遇到指标含义不确定直接查这里
BPF Performance Tools (Brendan Gregg) - eBPF 性能工具的系统性参考书,第九章专门讲磁盘 IO 追踪
fio Documentation - fio 官方文档,参数说明最全,做基准测试前必读
Linux Storage Stack Diagram - Linux 存储栈可视化全景图,每个内核版本都有对应的更新版本
io_uring 内核文档 - io_uring 的内核侧官方文档,涵盖 API 设计和使用约束
liburing GitHub - io_uring 的用户态封装库,Jens Axboe 维护,示例代码是学习 io_uring 编程的最佳入口
bcc/libbpf-tools - biolatency、biosnoop 等工具的 libbpf 版本源码,比 Python 版性能更好
NVMe CLI - NVMe 设备管理和诊断的命令行工具集,支持 SMART 信息查看、固件更新、namespace 管理
Systems Performance, 2nd Edition (Brendan Gregg) - 系统性能分析的经典著作,磁盘 IO 章节覆盖了从原理到工具的完整知识体系