Linux 磁盘 IO 飙高,别慌,按这几步排查
一、问题背景
磁盘 IO 是绝大多数 Linux 服务器最容易被忽视的那一环。CPU 飙高,运维同学往往能快速反应过来;但磁盘 IO 飙高时,监控曲线像锯齿,业务接口却在缓慢超时,DBA 同事和业务方都会把锅甩过来。
这些年我经手过几次典型的磁盘 IO 故障,触发原因五花八门:
- 业务上线了一个全表扫描的定时任务,瞬时读 IO 把 SSD 的吞吐打到瓶颈;
- 备份脚本写错路径,把
/var/log 整个目录重定向到数据库所在的磁盘; - 大量日志写入引发 dirty page 飙涨,内核只能疯狂回写;
vm.dirty_ratiodrop_caches 在生产环境被半夜值班同学随手触发,触发瞬间所有冷数据回源到磁盘;- cgroup v1 的 blkio 限制写错了单位,把磁盘限速到几 KB,结果业务全挂;
- 共享同一台物理磁盘的别的业务在做备份,IO 被争抢;
- NUMA 节点绑错,CPU 跨节点访问磁盘设备,链路抖动。
这些场景的根因分布在不同的层:文件系统层、块设备层、IO 调度器层、页缓存与回写层、cgroup 层。排查的难点不是工具不会用,而是不会组合:每个工具有它擅长的视角,孤立地看很难拼出全图。
这一篇把多年来沉淀的磁盘 IO 排查方法拆开讲清楚,目标是让你在线上环境遇到 iostat 列里 await / svctm / %util 飙红时,能按一个固定流程在 10 到 30 分钟内定位到进程甚至文件级别。
二、适用场景
本文描述的排查流程适用于以下情况:
- 业务响应变慢、
top 看到 wa 列(iowait)持续超过 20%; - 监控告警
node_disk_io_time_weighted / node_disk_io_now / node_disk_reads_completed / node_disk_writes_completed 中任意一项超过基线; - 数据库的 RTT / RPS 突然掉档,但 CPU、内存、网络使用率都正常;
- 应用日志里出现大量
io timeoutdisk slowIO ErrorStalled IO 关键字; - 容器集群里某个 Pod 内的进程出现明显的磁盘 IO 等待;
- 云服务器出现 IO 限速(云厂商对突发 IO / IOPS 有上限);
- 物理机主板固件或 RAID 卡出现异常,导致磁盘响应异常。
本文不覆盖的场景:
- NFS / CIFS / GlusterFS / CephFS 等网络文件系统的性能问题:这些文件系统的瓶颈在网络和元数据服务器,请结合
nfsstat / mountstats 等工具单独排查; - 虚拟化层 IO 虚拟化过度订阅问题(ESXi / KVM virtio-blk 队列拥堵):需要从宿主机和客户机两侧同时抓数据;
- 数据库自身的 buffer pool 抖动问题:本文只在和页缓存、内核回写相互影响的角度提一下,详细调优见后续 MySQL 相关文章;
- 文件系统本身的 bug:建议先
dmesg 看内核 oops,再升级内核 / 文件系统补丁。
三、核心知识点
3.1 Linux IO 子系统的纵向分层
一次 read() 系统调用,从应用到硬件,依次穿过:
- VFS 层:进程发起
read() / write(),VFS 做权限检查并把请求转交给具体文件系统; - 文件系统层:ext4 / xfs / btrfs 等不同具体文件系统生成各自的
bio 结构,包含目标扇区号、长度、读 / 写标志; - 通用块层:把 bio 合并、排序,加入对应请求队列
request_queue; - IO 调度器层:
mq-deadline / bfq / none / kyber 等基于多队列对上一步的请求做调度; - 设备驱动层:PCIe NVMe 走 NVMe 驱动,SAS / SATA 走 ata / SCSI 驱动;
- 硬件层
排查 IO 时一定要先想清楚:当前看到的指标是这一层,还是那一层。
iostatiotopblktraceperf- 云厂商给的 IOPS 限流,是把整个路径强制截短。
3.2 关键指标体系:每一列到底在说什么
iostat -x 1 输出的字段,对绝大多数人来说是最关键的入口:
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
nvme0n1 120.00 450.00 15360.00 8192.00 0.00 120.00 0.00 21.05 1.20 18.40 8.50 128.00 18.20 0.80 95.00
逐字段解释:
- r/s / w/s
- rkB/s / wkB/s
- rrqm/s / wrqm/s:被合并的请求数,相邻扇区的请求会被内核合成更大的请求;
- %rrqm / %wrqm:请求合并率,过低说明上层发散写入(典型:日志多线程并发写);
- r_await / w_await:请求从进入设备队列到完成的总等待时间,单位 ms,含排队和服务;
- aqu-sz
- rareq-sz / wareq-sz:平均每次请求大小,单位 KB,机械盘随机 IO 时这个值会非常小;
- svctm:设备平均服务时间,2.x 内核后已不可靠,只能参考;
- %util:设备忙于 IO 的时间比例。当 IO 不能并发执行时(机械盘单队列),
%util 接近 100% 一定意味着饱和;但 NVMe SSD 并发能力很强,%util 100% 并不意味着真的满载。
判断方法:
r_await 高、w_await 低:瓶颈在读,可能是数据库热数据被踢出缓存;w_await 高、r_await 低:瓶颈在写,可能是日志或回写压力;await 高、%util 不高:等待发生在队列层、设备没饱和,常见于调度器配置或队列深度不足;await 不高、%util 100%:典型 NVMe 场景,设备并发能力强,看似繁忙但响应很快;aqu-sz 大、%util 高、await 高:设备真的饱和。
3.3 IO 调度器:mq-deadline / bfq / none / kyber 该选谁
Linux 现代内核(5.x 后)默认使用多队列的 mq-deadline 或 none:
- none:完全不调度,直通到设备,给 NVMe SSD 这种自带队列的设备用最佳;
- mq-deadline:把请求分组为 read / write FIFO,对读有期限,对写有批量,通用性强;
- bfq:基于预算的公平调度,对桌面和延迟敏感场景友好,但吞吐略低;
- kyber
机械盘:建议 mq-deadline 或 bfq; NVMe SSD:建议 none; SATA SSD:可以试 mq-deadline 或 bfq。
3.4 页缓存与回写:那一半"看不见的 IO"来自内核
free 命令看到的 buff/cache 字段就是页缓存。应用写文件时,绝大多数情况下写到了页缓存而非真正落盘。内核有专门的回写线程 pdflush / writeback 把脏页刷到磁盘。
关键参数:
vm.dirty_ratio(默认 20):单个进程脏页占总内存比例的硬上限,达到后该进程同步回写;vm.dirty_background_ratio(默认 10):所有脏页比例超过此值时,后台回写线程开始工作;vm.dirty_expire_centisecsvm.dirty_writeback_centisecsvm.dirtytime_expire_seconds(默认 86400 秒):用于跟踪 inode 元数据(ctime/mtime)脏页寿命,与 vm.dirty_* 是并列两套机制,由内核写回时一并处理,对 ext4 / xfs 均生效。
很多人以为只要有 dirty_* 参数在就没事,实际上生产环境的常见情况是:
- 内存大(如 256GB),
dirty_ratio=20 含义是 51GB 脏页,意味着顶峰期有大量数据待写; - 业务大量顺序写日志,脏页堆积后写一张 SSD 跟不上;
dirty_background_ratiodirty_expire_centisecs
排查时要观察 nr_dirty / nr_writeback / nr_unstable 这几个值:
cat /proc/vmstat | egrep 'dirty|writeback'
如果 nr_writeback 持续很高,说明回写线程一直很忙;如果 nr_dirty 持续很高,说明写入速率超过回写速率。
3.5 工具链一览
| | |
|---|
iostat | | |
iotop | | |
pidstat -d | | |
vmstat | | 看 swap / cs / wa,关注 bi / bo |
dstat | | |
blktrace | | |
perf | | |
bpftrace | | |
fio | | |
dd | | |
smartctl | | |
iotop 和 pidstat -d 的区别:iotop 在很忙的机器上会有较大开销(每行打印会触发 /proc 读取),适合短时间观察;pidstat -d 是采样式的,适合放进监控脚本。
四、整体排查思路
4.1 由顶到底的分层定位
无论 IO 表现为什么样子,都建议按下面这个顺序逐步深入:
1. 顶层判断:是不是真的是 IO?
- top 看 %wa
- uptime 看 load average 跟 CPU 核数差距
- vmstat 1 看 bi / bo / cs / us / sy / id / wa
- 确认当前指标是不是 IO 主导
2. 全局层:iostat -x 1 5
- 哪块盘在忙
- 是读主导还是写主导
- 延迟分布
- 是否有大量合并请求
3. 进程层:iotop / pidstat -d 1
- 哪个进程在写 / 读
- 是否能 kill 或限速
4. 文件层:lsof -p <pid>
- 进程打开了哪些文件
- 是否触犯了不该写的路径
5. 内核层:dmesg / /proc/vmstat / /proc/pressure/io
- 是否有 IO 错误
- 脏页状态
- psiri 压力
6. 调度层:cat /sys/block/*/queue/scheduler
- 当前调度器
- 队列深度
- readahead
7. 硬件层:smartctl -a / nvme smart-log / megacli / sas3ircu
- RAID 卡状态
- 磁盘 SMART
- 硬件告警
4.2 排查流程图
┌─────────────────────────┐
│ 收到"IO 慢了"的工单/告警 │
└─────────────┬───────────┘
│
┌──────────────▼──────────────┐
│ top / uptime: 是 %wa 高吗? │
└───────┬───────────────┬────┘
否 是
│ │
▼ ▼
排查 CPU / 内存 ┌──────────────────────────┐
│ iostat -x 1 5: 哪块盘? │
└──────────┬───────────────┘
│
┌─────────────────────────┼────────────────────┐
▼ ▼ ▼
读主导 写主导 混合
│ │ │
▼ ▼ ▼
iotop 找读进程 iotop 找写进程 多个盘?
pidstat -d 1 pidstat -d 1 各盘分别
看看谁在扫表 看 dirty* 回写情况 定位饱和盘
│
▼
dmesg / vmstat
dirty 比例 / nr_writeback
调度器 / 队列深度
│
▼
应用层修复
内核参数调整
IO 调度器切换
4.3 排查时的协作建议
遇到 IO 类故障,多半会跨团队。提前和开发、DBA、云厂商沟通可以减少反复取证:
- 找业务侧要最近变更清单:发布、定时任务、扩容、回滚;
- 找 DBA 要数据库的慢查询、锁等待、行锁、Buffer Pool;
- 找云厂商要 IOPS / 吞吐限速的真实值(很多云平台要在控制台翻才能看到);
- 找硬件供应商要 SMART 日志(云厂商往往要在工单系统里提单)。
五、实战步骤
下面按编号把每一步的执行细节展开。每个步骤都包含:目的 / 命令 / 预期输出 / 异常表现 / 判断逻辑 / 下一步动作。
5.1 第一步:iostat -x 1 5,看全局 IO
目的:判断是否真的存在 IO 瓶颈,是哪块盘,是读还是写。
命令:
# 安装 sysstat(RHEL/CentOS)
yum install -y sysstat
# Debian/Ubuntu
apt-get update && apt-get install -y sysstat
# 取样 5 次,间隔 1 秒
iostat -x 1 5
预期输出(健康):
Device r/s w/s rkB/s wkB/s aqu-sz await svctm %util
nvme0n1 8.00 32.00 128.00 512.00 0.20 0.80 0.10 4.00
sda 0.10 0.05 4.00 1.00 0.00 0.50 0.05 0.10
异常表现:
判断逻辑:
- 多块盘都有问题:瓶颈在更高层(应用 / 内核 / 网络存储等);
- 持续写高但
await 不高:常见于 SSD 顺序大块写; - 持续写高
await 也高:很可能写饱和,物理跟不上了。
下一步动作:
- 用
df -h /<mountpoint> 看剩余容量; - 用
mount | grep <mountpoint> 看文件系统类型;
5.2 第二步:iotop -oP,定位进程元凶
目的:判断是哪个进程在读写这块盘。
命令:
yum install -y iotop # 安装
iotop -oP # 只显示有 IO 活动的进程,不显示 idle
参数说明:
预期输出(部分):
Total DISK READ: 0.00 B/s | Total DISK WRITE: 85.34 M/s
Current DISK READ: 0.00 B/s | Current DISK WRITE: 90.12 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
12345 ?Apid mysql 0.00 B/s 65.12 M/s 0.00 % 0.00 % mysqld
18890 ?Apid root 0.00 B/s 20.00 M/s 0.00 % 0.00 % python3 /opt/bak.sh
异常表现:
- 出现多个业务进程同时大量写:可能存在定时任务并发触发;
- 出现
bash / sh 写:可能是某个 cron job 或脚本; - 出现
sh / bash 写 /var/log:清理日志或转储业务; - 出现
rsync / tar / cp / dd:文件级搬迁。
判断逻辑:
下一步动作:
lsof -p <PID>ls -la /proc/<PID>/fdreadlink /proc/<PID>/cwdcat /proc/<PID>/io
风险提示:iotop -oP 必须以 root 运行;线上环境使用前要通知相关方,因为 IO 高时 iotop 自己也会产生少量开销。
5.3 第三步:pidstat -d 1 5,看进程级 IO 统计
目的:iotop 抓不住的细节(被剔出的进程、短暂的尖刺),用 pidstat 抓历史。
命令:
# 安装
yum install -y sysstat # pidstat 在 sysstat 包里
# 看所有进程,每秒 1 次,共 5 次
pidstat -d 1 5
# 看指定进程
pidstat -d -p 12345 1 5
# 看线程
pidstat -d -t -p 12345 1 5
参数说明:
-d-p-t-w:上下文切换(结合用)pidstat -dw 同时看 IO 和上下文切换;-u
预期输出(异常):
13:32:11 UID PID kB_rd/s kB_wr/s kB_ccwr/s Command
13:32:13 27 1234 0.00 6500.00 0.00 mysqld
13:32:14 27 1234 0.00 8500.00 0.00 mysqld
判断逻辑:
- 同一进程重复出现在多次采样中 → 这不是抖动,是持续 IO;
- 写入速率极高(GB/s 级别)→ 多半是逻辑卷 / RAID 重映射操作。
下一步动作:
- 拿到 PID 后用
lsof -p <PID> 看具体文件; ionice -p <PID>cat /proc/<PID>/io- 把可疑进程的 IO 优先级调低(class 3),前提是不能影响业务可用性。
5.4 第四步:vmstat 1,看内存与 IO
目的:判断 IO 高是不是内存回收 / swap 引发的,顺便看上下文切换与系统调用。
命令:
预期输出(健康):
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 0 204800 4096 409600 0 0 8 32 1024 2048 20 10 70 0 0
字段对照:
si / so:swap in / swap out,原则上应该是 0;bi / bo:blocks in / blocks out,块设备的读写数据量,单位 KB/s;csus / sy / id / wa:用户态 / 系统态 / 空闲 / iowait;b:处于不可中断睡眠状态的进程数(D 状态),IO 高时容易堆。
异常表现:
waso > 0:正在换出,物理内存不足,建议先加内存,不要先调 IO;b > 5cs > 100000
判断逻辑:
wawa 高 + so > 0:先解决内存(swap 引发的 IO 比应用 IO 难处理得多);wa 高 + b=0:可能是网络 IO(vmstat 不能区分)。
下一步动作:
- 看内存:
free -h、cat /proc/meminfo; - 看 swap:
swapon --show、vmstat 1; - 调出 D 状态进程:
ps -eo pid,stat,cmd | awk '$2 ~ /D/' 或 cat /proc/<PID>/stack。
5.5 第五步:dstat -cdngy,多维并行观察
目的:在 IO 飙高的同时记录 CPU、磁盘、网络、内存、页面活动,方便事后回溯。
命令:
yum install -y dstat
# 1 秒一次,输出 CSV 格式到文件
dstat -cdngy --output /tmp/ds.csv 1 60
# 不写文件,就看屏幕
dstat -cdngy 1 30
参数:
预期输出(异常):
----total-cpu-usage---- -dsk/total- -net/total- ---paging-- ---system--
usr sys idl wai hiq siq| read writ| recv send| in out | int csw
20 10 0 70 0 0| 12M 156M| 0 0 | 0 0 |1230 3400
判断逻辑:
下一步动作:
- 输出到 CSV 后可以交给同事在 Excel 里画图;
- 关联
pidstat -d 一起用,定位具体进程;
5.6 第六步:perf trace / bpftrace biosnoop,看内核栈
目的:当 iotop 都抓不到峰值(因为持续时间太短),需要 tracepoint 工具。
命令(bpftrace):
# 安装 bpftrace
yum install -y bpftrace
# 抓到一段后 Ctrl+C 退出
bpftrace -e '
kprobe:blk_mq_start_request {
printf("%d %s %d\n", pid, comm, args->rq->cmd_flags);
}
'
更复杂的脚本(biosnoop):
bpftrace -e '
tracepoint:block:block_rq_issue
/args->rwbs == "W"/ { @start[arg0] = nsecs; }
tracepoint:block:block_rq_complete
/args->rwbs == "W"/ /@start[arg0]/ {
@usecs = hist((nsecs - @start[arg0]) / 1000);
delete(@start[arg0]);
}
'
interval:s:1 { print(@usecs); clear(@usecs); }
'
判断逻辑:
- 内核栈卡在
blk_mq_start_request 队列 → 队列深度饱和; - 卡在
__blk_mq_run_hw_queue → 调度器问题; - 卡在
filemap_get_pages → 页缓存问题。
下一步动作:
- 调整调度器或队列深度:
/sys/block/<dev>/queue/scheduler; - 调大容量队列:
echo 1024 > /sys/block/<dev>/queue/nr_requests; - 确认驱动版本:
ethtool -i <dev> / modinfo nvme。
5.7 第七步:fio 做磁盘基线测试
目的:在定位进程后,判断磁盘本身是否还"健康"。如果 fio 也跑不出标称性能,那是物理 / 云端 IO 限速。
命令:
yum install -y fio
# 顺序写 4GB,评价盘能力
fio --name=seq_write --filename=/mnt/test.bin \
--rw=write --bs=1M --size=4G --numjobs=1 \
--runtime=60 --time_based --ioengine=libaio \
--direct=1 --group_reporting
# 随机写 4K(最常见的数据库场景)
fio --name=rand_write_4k --filename=/mnt/test.bin \
--rw=randwrite --bs=4k --size=4G --numjobs=4 \
--runtime=60 --time_based --ioengine=libaio \
--direct=1 --iodepth=32 --group_reporting
# 混合读写 70/30
fio --name=mix --filename=/mnt/test.bin \
--rw=randrw --rwmixread=70 --bs=4k --size=4G \
--runtime=60 --time_based --ioengine=libaio \
--direct=1 --iodepth=32 --group_reporting
风险提醒:fio 必须在不会影响业务的空闲盘或临时文件路径执行。误在 /var/lib/mysql 执行会直接打爆数据库所在盘;务必先用 --directory <dir> 测试;测试前确认 fio --eta=never 时不能阻塞其他作业。
判断逻辑:
- 顺序写达不到盘标称带宽:硬件或 RAID 卡问题;
- 随机 4K 写远低于标称 IOPS:盘老化或云厂商限制;
- 随机读写抖动大:磁盘控制器或 RAID BBU 问题。
下一步动作:
- 把测试结果和厂商标称对比,差距 30% 内算正常;
- 对比
iostat -x 和 fio 的延迟分布;
5.8 第八步:判断 IO 调度器与队列深度
目的:IO 调度器和队列深度对延迟影响极大,需要在系统层确认是否合理。
命令:
# 当前调度器
cat /sys/block/nvme0n1/queue/scheduler
# 输出:[mq-deadline] kyber bfq none
# 切换调度器(立即生效,重启失效)
echo kyber > /sys/block/nvme0n1/queue/scheduler
# 队列深度
cat /sys/block/nvme0n1/queue/nr_requests
# 当前 IO 队列深度
cat /sys/block/nvme0n1/queue/queue_depth
# readahead
cat /sys/block/nvme0n1/queue/read_ahead_kb
# rotational(0 表示非转动盘,1 表示转动盘)
cat /sys/block/nvme0n1/queue/rotational
判断逻辑:
- NVMe 盘最佳调度器是
none,但部分内核(4.x)默认还是 mq-deadline,需要手动切; nr_requests 默认可能只有 128,机械盘随机 IO 大时建议调高到 256-512;read_ahead_kb 默认可能是 128 KB,机械盘顺序读场景调到 1024+,随机读场景调到 128 不要改。
下一步动作:
cat > /etc/udev/rules.d/60-io-scheduler.rules << 'EOF'
# 为 nvme 设备设置 none 调度器
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
# 为 sata ssd 设置 mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
# 为 hdd 设置 bfq
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
EOF
udevadm control --reload-rules
udevadm trigger
- 在
/etc/sysctl.d/99-io.conf 调内核参数(详见配置示例一节)。
5.9 第九步:/proc/pressure/io 看 PSI 压力
目的:内核 4.20+ 开始提供 Pressure Stall Information,能更准确地衡量 IO 对进程的拖累。
命令:
cat /proc/pressure/io
# some avg10=0.00 avg60=0.00 avg300=0.00 total=0
# full avg10=0.00 avg60=0.00 avg300=0.00 total=0
字段说明:
some 行:至少有 1 个进程在等 IO 的时间比例(5 秒内);fullavg10 / avg60 / avg300total
判断逻辑:
下一步动作:
- 在 Prometheus
pressure_io_waiting_seconds_total 中告警(详见第八节); - 如果只
full 高,多半是单进程(如数据库)被 IO 卡死。
5.10 第十步:smartctl -a / nvme smart-log,最后看硬件
目的:所有软件层都查完还没解决,看硬件。
命令:
yum install -y smartmontools
# SATA 机械盘 / SATA SSD
smartctl -a /dev/sda
# NVMe
smartctl -a /dev/nvme0n1
# 短自检
smartctl -t short /dev/sda
# 长自检(后台)
smartctl -t long /dev/sda
# 看自检日志
smartctl -l selftest /dev/sda
# 看错误日志
smartctl -l error /dev/sda
判断逻辑:
Reallocated_Sector_Ct(SMART 5)> 0:磁盘已经有坏块重映射,继续上涨说明盘在死亡;Current_Pending_Sector(SMART 197)> 0:等待重映射的扇区,临近危险;Offline_Uncorrectable- NVMe
critical_warning 非 0:硬件告警; Percentage Used
下一步动作:
- 用
ddrescue / dd 抢救数据前先评估。
风险提示:smartctl -t long /dev/sda 是后台长自检,会加大 IO 压力;生产环境避免在负载高峰执行。
六、常用命令汇总
下面这些命令已经在我自己的环境里沉淀成了 IO 故障排查的"急救包"。
6.1 综合信息收集脚本
#!/bin/bash
# io-snapshot.sh - 一次性采集 IO 故障的现场信息
# 用法: sudo bash io-snapshot.sh
# 注意: 不修改任何数据,只读采集
OUT=/tmp/io-snapshot-$(date +%Y%m%d-%H%M%S)
mkdir -p "$OUT"
cd"$OUT" || exit 1
echo"[*] 采集系统基本信息 ..."
uname -a > system.info
cat /etc/os-release >> system.info
date >> system.info
uptime >> system.info
free -h > mem.free
cat /proc/meminfo > mem.proc
echo"[*] 采集块设备层指标 ..."
iostat -x 1 5 > iostat.txt 2>&1
iostat -d 1 5 > iostat-d.txt 2>&1
lsblk > lsblk.txt 2>&1
df -h > df.txt 2>&1
mount > mount.txt 2>&1
echo"[*] 采集进程级指标 ..."
top -bn2 > top.txt 2>&1
ps auxf > ps.txt 2>&1
ps -eo pid,ppid,stat,etime,comm > ps-stat.txt 2>&1
echo"[*] 采集 IO 重点进程 ..."
for p in $(ps -eo pid --sort=-%cpu | head -20); do
[ -d /proc/$p ] || continue
echo"PID=$p comm=$(cat /proc/$p/comm 2>/dev/null)" >> proc.io
cat /proc/$p/io 2>/dev/null >> proc.io
echo"----" >> proc.io
done
echo"[*] 采集调度器与内核参数 ..."
for d in /sys/block/*; do
echo"$d:"
echo" scheduler=$(cat $d/queue/scheduler)"
echo" nr_requests=$(cat $d/queue/nr_requests)"
echo" read_ahead_kb=$(cat $d/queue/read_ahead_kb)"
echo" rotational=$(cat $d/queue/rotational)"
cat$d/stat 2>/dev/null | awk '{print " stat="$0}'
done > scheduler.txt
sysctl vm.dirty_* > sysctl-dirty.txt 2>&1
cat /proc/vmstat > vmstat.txt 2>&1
cat /proc/pressure/io > psi-io.txt 2>&1
echo"[*] 采集日志与硬件状态 ..."
dmesg -T > dmesg.txt 2>&1
dmesg -T | grep -iE 'i/o|error|ata|nvme|scsi' > dmesg-io.txt 2>&1
last -100 > last.txt 2>&1
tar czf /tmp/io-snapshot.tgz "$OUT"
echo"[*] 已打包到 /tmp/io-snapshot.tgz"
ls -la /tmp/io-snapshot.tgz
注意:
- 在 /tmp 下操作,但 /tmp 可能被限制目录权限,可以在脚本开头确认;
- 退出码要保留,建议加
set -e,但别让失败中断整体采集,把每个 echo 都包到 “尝试” 段。
6.2 一句话现场抓取命令
# top 排序进程
top -bn1 -o %CPU | head -20
# iotop 单次快截图
iotop -bn1 -oP | head -20
# pidstat 单次抓
pidstat -d 1 5 | tail -20
# vmstat 单次抓
vmstat 1 5
# dstat 多维单次抓
dstat -cdngy 1 5
# 找出所有 D 状态进程并显示内核栈
ps -eo pid,ppid,stat,etime,comm | awk '$2 ~ /D/'
for p in $(ps -eo pid,stat | awk '$2 ~ /D/{print $1}'); do
echo"== PID $p =="
cat /proc/$p/stack 2>/dev/null
echo
done
6.3 调度器与内核参数快速调整
# 看当前调度器
cat /sys/block/*/queue/scheduler | uniq -c
# 切到 none
echo none | tee /sys/block/nvme*/queue/scheduler
# 提高 nr_requests
echo 512 > /sys/block/sda/queue/nr_requests
# 调 read_ahead
echo 1024 > /sys/block/sda/queue/read_ahead_kb
# 调内核 dirty 参数(临时)
sysctl -w vm.dirty_ratio=10
sysctl -w vm.dirty_background_ratio=5
6.4 监控数据采集脚本
#!/bin/bash
# io-record.sh - 长期记录机器 IO 状态到 log
INTERVAL=${1:-5}
LOG=/var/log/io-record-$(hostname).log
mkdir -p $(dirname"$LOG")
echo"ts,rrqm/s,wrqm/s,r/s,w/s,rkB/s,wkB/s,avgrq-sz,avgqu-sz,await,svctm,%util" > "$LOG"
iostat -dx "$INTERVAL" | awk -v OFS=',''
NR > 3 && $1 ~ /^[a-z]/ {
print systime(), $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13
}
' >> "$LOG"
用 nohup 让它在后台跑:
chmod +x io-record.sh
nohup ./io-record.sh 5 >/dev/null 2>&1 &
风险提示:这种后台监控脚本自身有少量 IO 开销,在已经爆满的盘上可能进一步加剧问题,建议在排查场景而不是长期监控中使用。
七、配置示例
7.1 IO 调度器 udev 规则
cat > /etc/udev/rules.d/60-io-scheduler.rules << 'EOF'
# NVMe -> none(直通)
ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="none"
# SATA SSD / 虚拟机虚拟盘 -> mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
# 机械盘 -> bfq 或 mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
EOF
udevadm control --reload-rules
udevadm trigger
验证:
lsblk -o NAME,SCHED
# NAME SCHED
# nvme0n1 none
# sda bfq
7.2 内核 dirty 参数
/etc/sysctl.d/99-io-dirty.conf:
# 控制内核脏页比例
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
# 老化时间
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 100
# 边界保护
vm.dirtytime_expire_seconds = 43200
参数说明:
dirty_background_ratiodirty_ratiodirty_expire_centisecsdirty_writeback_centisecs
风险提示:把 dirty_background_ratio 调成 1 等激进值会让回写线程长期处于满负载,反而造成 IO 抖动,慎用。
应用:
sysctl --system
# 或
sysctl -p /etc/sysctl.d/99-io-dirty.conf
7.3 cgroup v1 blkio 限制
/etc/systemd/system/io-limit.slice:
[Unit]
Description=IO limited slice
DefaultDependencies=no
[Slice]
CPUWeight=100
MemoryHigh=2G
IOWeight=100
IOReadBandwidthMax=/dev/nvme0n1 50M
IOWriteBandwidthMax=/dev/nvme0n1 30M
IOReadIOPSMax=/dev/nvme0n1 5000
IOWriteIOPSMax=/dev/nvme0n1 4000
校验命令:
systemd-cgtop
# 看哪个 slice 占多少 IO
# 永久生效
systemctl set-property my-service.slice IOReadBandwidthMax=/dev/nvme0n1 50M
systemctl set-property my-service.slice IOWriteBandwidthMax=/dev/nvme0n1 30M
风险提示:
IOWeight 是相对权重,不是带宽上限。如要硬上限用 IOReadBandwidthMax / IOWriteBandwidthMax;- 在 cgroup v1 blkio 老版本内核(3.x)中,限制不一定精确;
- cgroup v2 在 RHEL 8 / CentOS 8+ / Ubuntu 22+ 上效果更好,建议升级。
7.4 cgroup v2 IO 限制示例
/etc/systemd/system/io-limit-v2.slice:
[Slice]
CPUWeight=100
MemoryHigh=2G
IOWeight=100
IOReadBandwidthMax=/dev/nvme0n1 50M
IOWriteBandwidthMax=/dev/nvme0n1 30M
cgroup v2 在 mount | grep cgroup2 出现的机器上工作。
7.5 NVMe 多队列优化
/etc/modprobe.d/nvme.conf:
# NVMe 多队列
options nvme io_timeout=30 poll_queues=8
应用:
dracut -f # RHEL/CentOS
update-initramfs -u # Debian/Ubuntu
风险提示:内核参数调整必须先在测试机验证,再灰度到生产;dracut -f 会重新生成 initramfs,执行失败的话系统可能无法启动;建议保留旧 initramfs。
7.6 sysctl 完整推荐值(参考)
/etc/sysctl.d/99-io.conf:
# 脏页控制
vm.dirty_background_ratio = 5
vm.dirty_ratio = 20
vm.dirty_expire_centisecs = 1500
vm.dirty_writeback_centisecs = 100
# 内存回收优化
vm.swappiness = 10
# IO 调度相关
vm.page-cluster = 0
# 文件系统缓存压力
vm.vfs_cache_pressure = 50
# PSI(Pressure Stall Information)由内核 4.20+ 自动开启,
# 关闭需要在 boot 阶段加 psi_disabled 参数;此处不通过 sysctl 控制。
风险提示:swappiness=10 是相对激进的设置(避免用 swap),但对某些数据库场景不友好;page-cluster=0 在高 IO 场景下可能反向优化,需要按机器实际负载调。
# 验证 swappiness
cat /proc/sys/vm/swappiness
# 验证 vfs_cache_pressure
cat /proc/sys/vm/vfs_cache_pressure
# 验证 PSI io
cat /proc/pressure/io
八、日志与指标观察方法
8.1 dmesg 中的 IO 关键字
dmesg -T | grep -iE 'i/o error|read error|write error|medium|timeout|nvme|ata|scsi'
关键关键字:
I/O errorMedium Error / Hardware Error:硬件已经出故障;device timeoutresetting linknvc=nvme0n1: Device not readysoft lockup:内核长时间没调度,配合 B 状态判断是否 IO 引发的。
8.2 /sys/block/*/stat 的字段含义
cat /sys/block/nvme0n1/stat
# 字段 read_io read_merged read_sectors read_ticks write_io write_merged write_sectors write_ticks io_in_progress io_ticks io_ticks_total
read_merged / write_mergedread_ticks / write_ticksio_in_progressio_ticks_total
这种 stat 可以脚本化采集,做长期趋势:
cat /sys/block/nvme0n1/stat | awk '{print "read_io="$1, "write_io="$5, "io_in_progress="$9}'
8.3 Prometheus 监控指标
node_exporter 暴露的 IO 关键指标:
| | |
|---|
node_disk_io_now | | |
node_disk_io_time_seconds_total | | |
node_disk_io_time_weighted_seconds_total | | |
node_disk_reads_completed_total | | |
node_disk_writes_completed_total | | |
node_disk_read_bytes_total | | |
node_disk_writes_bytes_total | | |
node_disk_read_time_seconds_total | | |
node_disk_write_time_seconds_total | | |
node_disk_*_merged_total | | |
node_disk_pending_operations | | |
node_pressure_io_waiting_seconds_total | | |
node_pressure_io_stalled_seconds_total | | |
推荐告警规则示例(PromQL):
# IO 队列堆积
-alert:DiskIOPendingHigh
expr:node_disk_pending_operations>32
for:5m
# IO 利用率
-alert:DiskIOUtilHigh
expr:|
rate(node_disk_io_time_seconds_total[5m]) > 0.85
for:5m
# 单盘读写延迟
-alert:DiskIOLatencyHigh
expr:|
(rate(node_disk_read_time_seconds_total[5m]) /
rate(node_disk_reads_completed_total[5m])) * 1000 > 20
for:5m
# PSI some 持续高
-alert:PsiIOSomeHigh
expr:|
rate(node_pressure_io_waiting_seconds_total[5m]) > 0.3
for:5m
说明:
- 块设备名匹配规则是
node_disk_* 自动发现 /sys/block 下设备; - 容器场景需要 cAdvisor 提供
container_fs_* 指标。
8.4 业务侧 IO 行为采集
除了内核级,业务侧的 IO 行为数据同样关键:
sar -b 1 5:sar 提供的 tps / rtps / wtps / bread/s / bwrtn/s 统计;sar -d 1 5pidstat -d- 数据库
SHOW GLOBAL STATUS LIKE 'Innodb_data_*':InnoDB 内部 IO; - 应用日志中的
io wait / flush 关键字。
yum install -y sysstat
sar -b 1 5
sar -d 1 5
风险提示:sar 是采样式,但对内核 proc 文件的访问有少量开销;线上环境建议在采样期间不要改其他内核参数。
8.5 长期趋势分析方法
# 看 7 天前到今天的 IO 曲线(CSV)
zcat /var/log/sysstat/sa*.gz | sadf -d -- -p -r -j | grep -E 'nvme0n1|sda'
sadf 输出的是 RPN,可以导入 Grafana / Kibana。
8.6 把 IO 指标对接业务慢请求日志
业务侧的慢请求日志通常带 trace_id。在 OpenTelemetry 体系下,可以把 host 维度的 io.wait.time 注入到 trace span:
# OpenTelemetry Collector 配置示例
processors:
resourcedetection:
detectors: [system, ec2]
attributes/io:
actions:
-key:io.disk.await
from_attribute:host.disk.await
这样在慢请求 trace 里能直接看到这台机器当时的 IO 情况。
九、排查路径
针对不同现象,按决策树分以下几条典型路径。
9.1 %util 高但 await 不高
现象:
判断:设备并发能力很强(NVMe SSD),看似繁忙但延迟低,多半是业务真的有大量 IO 请求。
下一步:
- 检查应用是不是有非必要 IO(频繁写日志、无用快照);
- 如果没有业务优化空间,考虑扩容 IOPS(云厂商)或换更高规格的盘。
9.2 await 高但 %util 不高
现象:
判断:等待发生在队列层或调度器层,设备并没有真忙。
下一步:
- 检查是否被调度器的
min_budget 类机制限制。
9.3 IO 尖刺型(业务突然卡 30 秒)
现象:
常见原因:
- 系统 cron(如 logrotate、backup)定时触发;
- 内核
dirty_writeback_centisecs 唤醒导致一起回写;
下一步:
- 查看 systemd timer、cron 调度;
- 查看
logrotate / journald 配置;
9.4 持续写高负载(日志型业务)
现象:
判断:
下一步:
iotop- 改成异步日志(log4j/logger 加缓冲区)。
9.5 大量 random 读(数据库冷数据)
现象:
r/s 高、rkB/s 大、await 高、aqu-sz 高。
判断:
下一步:
- 看
SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_reads';
9.6 D 状态进程堆积
现象:
topps -eo stat | grep D | wc -l
判断:
下一步:
cat /proc/<pid>/stackdf -hmount | grep nfsnfsstat -m
9.7 swap 引发的 IO 风暴
现象:
判断:
下一步:
- 临时调
sysctl vm.swappiness=0(不能根除);
9.8 cgroup 限制引发的 IO 节流
现象:
iotopdmesg / syslog 出现 cgroup blkio throttle。
判断:业务在容器内或被 systemd slice 限速。
下一步:
9.9 云服务器 IO 限速
现象:
判断:典型云厂商基线型 IO 限速。
下一步:
9.10 RAID 卡 BBU / supercap 老化
现象:
megacli -LDInfo -Lall -aAll
判断:BBU 进入 Learn Cycle,写缓存被关闭。
下一步:
- 改写策略为 WriteThrough 配 WriteBack 切换;
十、风险提醒
磁盘 IO 排查涉及大量高风险操作,列出易踩的雷区。
10.1 误删磁盘上的数据
# 高危:清空整块磁盘
rm -rf /var/log/*
# 危险:直接 truncate 文件
: > /var/log/messages
风险:
rm -rf /var/log/* 可能误删正在打开的日志文件,导致 daemon 句柄失效;:> /var/log/messagesfind -delete 写得不对容易删错;建议在生产环境用 -print 加 -exec rm {} \; 或交互式 rm -i。
替代方案:
- 日志清理用
logrotate / journald --vacuum-time; - 引用
find 时加 -name 和 -maxdepth 严控范围。
10.2 drop_caches 的副作用
# 危险:清理页缓存
echo 3 > /proc/sys/vm/drop_caches
风险:
- 直接清空内核页缓存,后面的所有读会回到磁盘,IO 压力瞬间爆发;
- 已经飙升的 IO 加上突发 IO,机器可能直接卡死。
替代方案:
- 通过
posix_fadvise(POSIX_FADV_DONTNEED) 让应用主动释放; vmtouch
10.3 修改 dirty 参数的副作用
sysctl -w vm.dirty_ratio=5
风险:
- 对低内存机器上跑 Oracle、Postgres 等非常不友好;
替代方案:
- 在 5.x 内核上使用 cgroup v2 的
io.weight;
10.4 切换 IO 调度器风险
echo none > /sys/block/sda/queue/scheduler
风险:
- SATA SSD 切 none 可能反而性能更差;
- 切调度器本身不中断 IO,但磁盘的延迟分布瞬间变化,可能引起业务异常。
替代方案:
- 切之前先
perf record -a -g 一段;
10.5 cgroup 限制过严导致业务失败
systemctl set-property my.service IOReadBandwidthMax=/dev/nvme0n1 100M
风险:
替代方案:
- 用
IOWeight 而非 IOReadBandwidthMax,让 qos 弹性一些;
10.6 在已有 IO 瓶颈的盘上跑 fio
风险:fio 本身是 IO 压力源,会让已饱和的盘雪上加霜,可能引起节点 off-line。
替代方案:
- 用
cd /mnt/test; fio --filename=/mnt/test/io-test.bin --size=1G 不要打满整盘;
10.7 误用 blktrace 阻断生产
blktrace -d /dev/sda -o trace
风险:blktrace 在内核版本较老时会让 blk_remap 调用大幅增加,IO 路径上有阻塞时甚至会让机器无响应。
替代方案:
10.8 修改内核参数导致无法启动
# 假设重写 initramfs
dracut -f
风险:dracut 写错的话系统启动时会找不到 root。
替代方案:
- 保留上一版 initramfs:用
cp /boot/initramfs-*.img /boot/initramfs-*.bak; - dracut 完成后立即
dracut --force;
10.9 修改数据库配置导致雪崩
虽然这里不展开数据库参数,但 IO 排查中也可能涉及。注意:
innodb_io_capacityinnodb_flush_method=O_DIRECT- 改动后必须看
SHOW ENGINE INNODB STATUS 的 BUFFER POOL AND MEMORY 段。
10.10 在断错盘上跑 dd
ddif=/dev/zero of=/dev/sda bs=1M
风险:直接把系统盘数据清零。
替代方案:
- 永远确认
lsblk 输出后,再用 of=/dev/<确认的设备>; - 建议用
dd if=<file> of=/dev/null 做读测试;
十一、验证方式
排查完一项,必须知道怎么验证修复生效。下面把每条修复路径配对应的验证方法。
11.1 调度器切换验证
# 切换前
cat /sys/block/sda/queue/scheduler
# [mq-deadline] bfq none
# 切换
echo bfq > /sys/block/sda/queue/scheduler
# 验证
cat /sys/block/sda/queue/scheduler
# mq-deadline [bfq] none
# 跑 fio 验证延迟
fio --name=seq --filename=/mnt/test.bin \
--rw=read --bs=4k --size=1G --runtime=30 \
--time_based --ioengine=libaio \
--direct=1 --iodepth=32 --numjobs=1
判断:
11.2 dirty 参数验证
# 重启回写参数后观察
cat /proc/vmstat | grep -E 'nr_dirty|nr_writeback|nr_unstable'
判断:
11.3 cgroup 限制验证
systemd-cgtop
# 或
systemctl show my.service.slice -p IOReadBandwidthMax
验证:
11.4 drop_caches 修复后
仅盘可清空页缓存后用:
echo 1 > /proc/sys/vm/drop_caches # 清理页缓存
# 风险命令,仅在测试环境或业务允许的低峰期执行
风险提示:再次提醒,drop_caches 在生产环境是大杀器,会回源所有页面到磁盘,可能让数据库瞬间卡死。仅在已经做好停机计划或低峰期执行。
11.5 进程定位后的验证
- 找到进程 → kill → 观察 IO 是否恢复;
- kill 不可接受时 → 限速(
ionice 或 cgroup);
11.6 业务恢复验证
业务恢复后必须:
十二、回滚方案
每条修复动作必须有回滚预案。
12.1 调度器回滚
# 备份当前调度器
cat /sys/block/sda/queue/scheduler > /tmp/sched.before
# 应用切调度器
echo bfq > /sys/block/sda/queue/scheduler
# 验证后落地
# 出错时:
echo $(cat /tmp/sched.before | awk -F'[][]''{print $2}') \
> /sys/block/sda/queue/scheduler
# 持久化回滚:将 udev 规则改回
sed -i 's/ATTR{queue/scheduler}="bfq"/ATTR{queue\/scheduler}="mq-deadline"/' \
/etc/udev/rules.d/60-io-scheduler.rules
udevadm control --reload-rules
udevadm trigger
12.2 dirty 参数回滚
# 应用前快照
sysctl vm.dirty_ratio > /tmp/sysctl.dirty.bak
# 应用
sysctl -w vm.dirty_ratio=10
# 回滚
sysctl -w vm.dirty_ratio=$(cat /tmp/sysctl.dirty.bak | awk '{print $NF}')
# 持久化回滚
git -C /etc revert sysctl.d/99-io-dirty.conf --no-edit
风险提示:sysctl 即时生效,重启后通过 /etc/sysctl.d/*.conf 恢复。务必两边都改。
12.3 cgroup 限制回滚
# 应用前
systemctl show my.service.slice > /tmp/slice.before
# 应用后
systemctl set-property my.service.slice IOReadBandwidthMax=/dev/nvme0n1 50M
# 回滚
systemctl revert my.service.slice
# 或
systemctl set-property my.service.slice IOReadBandwidthMax=infinity
12.4 内核参数永久化回滚
# 在测试机先验证
# 回滚内核参数
sed -i 's/vm.dirty_background_ratio = 5/vm.dirty_background_ratio = 10/' /etc/sysctl.d/99-io-dirty.conf
sysctl -p /etc/sysctl.d/99-io-dirty.conf
12.5 业务层操作回滚
应用的回滚要跟随业务发布系统的能力:
kubectl rollout undo deployment/<dep>systemctl restart my.service
12.6 回滚窗口
生产环境所有 IO 相关修复必须配合回滚时间窗口:
- 关键指标(IO 利用率、P99 延迟、业务 RPS)回归基线;
十三、生产环境注意事项
13.1 操作窗口
- 涉及 IO 调度器、dirty 参数、cgroup 限制的改动,尽量放在业务低峰期(一般凌晨 0-6 点);
13.2 数据备份
任何 IO 相关的修复,先确认:
- 文件系统能用
xfsdump / ext4 snapshot 备份; fio
13.3 监控与告警
修复期间必须额外盯:
- iostat 类的
await / aqu-sz / %util;
13.4 灰度与金丝雀
13.5 沟通与文档
- 在工单或变更单里写明变更原因、操作、风险、回滚步骤;
13.6 与数据库团队的协作
数据库盘的 IO 改动必须和 DBA 协同:
ALTER TABLEOPTIMIZE TABLE / ALTER TABLE ... ENGINE=InnoDB 是 IO 大户;- 在线 DDL 工具(如 gh-ost)需要 DBA 评估。
13.7 云环境额外注意
- 云盘的 IOPS 会被突发的 burst 限制,运维操作要用 fio 测时把 burst 留着;
- 跨可用区的 IO 链路延迟比本机差,要重新算 IO 阈值;
- 公有云控制台和实机看到的 IO 指标有时差距,要以
node_exporter 数据为准。
13.8 容器云环境
- 容器内的
iotop / pidstat 不再准确(看到的 cgroup 视图); - 在宿主机上抓
docker top <container> 即可看到容器主进程; - Kubernetes
volume.beta.kubernetes.io/mount-options 永久设置调度器,但只对新挂载生效。
十四、总结
磁盘 IO 排查是工程实战的硬功夫。把上面的步骤收一收,按三层深度的顺序总结:
- 从现象出发:明确是
top 上的 wa 还是 iostat 上的 await,先确认 IO 是不是主导。 - 从全局到进程:先
iostat 看盘,再 iotop / pidstat 找进程,最后 lsof 找文件。 - 从进程到内核:从进程的 IO 栈往内核栈看,必要时打开
blktrace / bpftrace。 - 从内核到硬件:dmesg、SMART、RAID 卡、BIOS 固件版本逐层验证。
- 修复后再验证:调度器切换、dirty 参数调整、cgroup 限制修改都必须配合回滚与监控。
- 生产纪律:备份先行、灰度推开、回滚预案、沟通同步、复盘归纳。
最后再强调一次雷区:
drop_cachesrm -rffio- 内存不够的机器上 sysctl 救不了,盘才是源头。
把上面的命令、配置、决策树放在自己工位的备忘录里,再遇到 iostat 飙红就不用慌。