dbaplus社群

磁盘 IO 飙升导致系统卡死?iostat 与 iotop 深度实战

一、概述

1、背景介绍

监控大屏上 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 问题"到"定位根因"再到"调优解决"的完整链路。

2、技术特点
  • 全栈视角:从 VFS 到块设备驱动,逐层拆解 IO 路径,不停留在工具表面。

  • 工具链完整:iostat 看全局、iotop 定进程、blktrace 追请求、fio 做基准、bpftrace 抓延迟。

  • 面向实战:每个工具都给出真实场景下的输出解读,不是照搬 man page。

  • 调优闭环:不只是发现问题,还覆盖 readahead、dirty ratio、调度器等内核参数调优。

3、适用场景
  • 场景一:生产环境突发 IO 飙升,系统响应变慢甚至卡死,需要快速定位是哪个进程在疯狂读写。

  • 场景二:数据库服务器 IO 延迟周期性升高,需要区分是磁盘性能不足还是应用层 IO 模式有问题。

  • 场景三:新机器上线前需要做磁盘性能基准测试,评估 SSD/HDD 是否满足业务 IOPS 和吞吐需求。

  • 场景四:容器环境下多个 Pod 共享磁盘,需要找出 IO 资源的争抢源头。

4、环境要求
组件
版本要求
说明
操作系统
Ubuntu 24.04 LTS / RHEL 9.x
内核 6.8+,支持 io.latency cgroup 控制器
sysstat
12.7+
提供 iostat、sar 等工具
iotop
0.6+ / iotop-c 1.26+
进程级 IO 监控,推荐 iotop-c(C 语言重写版)
blktrace
1.3+
块设备层追踪
fio
3.37+
磁盘基准测试
bpftrace
0.21+
eBPF 追踪 IO 延迟分布
perf
6.8+
内核性能分析

二、Linux IO 栈全景与调度器

要排查 IO 问题,首先得搞清楚一个 IO 请求从应用程序发出到磁盘完成,中间经过了哪些环节。不理解 IO 栈的层次结构,看 iostat 的输出就只是在看数字。

1、IO 栈分层架构
应用程序 (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 性能。

2、IO 调度器详解

内核 6.x 提供了四种 IO 调度器,针对不同设备类型选择合适的调度器对性能影响很大。

# 查看当前磁盘使用的调度器(方括号标记的是当前生效的)cat /sys/block/sda/queue/scheduler# 输出示例:[mq-deadline] kyber bfq none# 运行时切换调度器(立即生效,不需要重启)echo "bfq" > /sys/block/sda/queue/scheduler

四种调度器的选型策略:

调度器
适用场景
核心机制
推荐设备
none
NVMe SSD
不做任何调度,直接下发到硬件队列
NVMe SSD(硬件自带调度)
mq-deadline
通用场景
按截止时间排序,防止请求饿死,读优先于写
SATA SSD、HDD、虚拟机云盘
bfq
桌面/混合负载
按进程公平分配 IO 带宽,类似 CPU 的 CFS
HDD、需要 IO 公平性的多租户场景
kyber
低延迟 SSD
基于目标延迟的轻量级调度,自动调节队列深度
高性能 SATA SSD

选型建议: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
3、 SSD vs HDD 的 IO 特性差异

理解 SSD 和 HDD 的物理特性差异,是正确解读 IO 指标的前提。

特性
HDD
SATA SSD
NVMe SSD
随机读 IOPS
100-200
30K-90K
200K-1M+
随机写 IOPS
100-200
20K-70K
100K-500K+
顺序读吞吐
150-250 MB/s
500-560 MB/s
3-7 GB/s
顺序写吞吐
150-250 MB/s
400-530 MB/s
2-5 GB/s
平均延迟
5-15ms
0.05-0.1ms
0.01-0.03ms
队列深度
1(NCQ=32)
32(NCQ)
64K x 多队列
%util
 参考意义
高(机械臂同一时刻只能服务一个位置)
低(内部并行度高)
极低(不要看这个指标)

这里有一个非常关键的认知:%util 对 SSD 几乎没有参考价值。HDD 是单通道设备,%util 100% 确实意味着磁盘忙不过来。但 SSD 内部有大量并行通道,%util 100% 可能只用了实际能力的 10%。判断 SSD 是否到达瓶颈,应该看 await(IO 延迟)和实际 IOPS 是否接近设备标称值。

三、iostat 输出深度解读

iostat 是 IO 排查的第一站,但它的输出字段很多,不少人只会看 %util,这远远不够。

1、 iostat 基础用法
# 最常用的命令:每秒刷新一次,显示扩展信息,单位用 MB# -x 扩展模式  -m 单位MB  -t 显示时间戳  1 间隔1秒iostat -xmt 1
# 只看特定磁盘iostat -xmt -d nvme0n1 sda 1
# 看第一次输出要注意:第一行是系统启动以来的累计平均值,不是实时数据# 从第二行开始才是每秒的实时数据,排查问题时忽略第一行
2、 输出字段逐个拆解

一条典型的 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/s
 / w/s
每秒完成的读/写请求数(IOPS)
HDD 超过 150 就要警惕,NVMe 到 10 万都正常
rkB/s
 / wkB/s
每秒读/写的数据量(吞吐)
对比磁盘标称带宽,接近上限说明带宽打满
rrqm/s
 / wrqm/s
每秒合并的读/写请求数
合并率高说明应用的 IO 模式比较友好(顺序或相邻)
r_await
 / w_await
读/写请求的平均耗时(ms),包含队列等待+设备服务时间
最重要的延迟指标
。HDD 正常 5-15ms,SSD 正常 0.05-0.5ms
aqu-sz
平均请求队列长度
队列长说明设备处理不过来,请求在排队
%util
设备繁忙时间百分比
HDD 有参考意义,SSD 参考价值低(前面解释过)
rareq-sz
 / wareq-sz
平均请求大小(KB)
判断 IO 模式:4-8KB 是典型随机 IO,128KB+ 是顺序 IO

已废弃的 svctm:老版本 iostat 有 svctm(设备服务时间)字段,sysstat 12.x 已经标记为不可靠并计划移除。这个值是用 %util / (r/s + w/s) 反算出来的,在多队列设备上完全失真。不要再用 svctm 做任何判断。

3、 iostat 实战判断模板
# 场景判断速查:# 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 模式
4、用 sar 看 IO 历史趋势

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 来定位到具体进程。

1、 iotop 基础用法
# 需要 root 权限(依赖内核的 taskstats 接口)# -o 只显示有 IO 活动的进程(过滤掉空闲的)# -P 显示进程而不是线程# -a 累积模式(显示自启动以来的累计 IO 量)sudo iotop -oP
# 推荐使用 iotop-c(C 语言重写版,性能更好)# Ubuntu: sudo apt install iotop-csudo iotop-c -oP
2、 iotop 输出解读
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/s    TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND  12847 be/4  mysql     98.20 M/s    5.60 M/s   0.00 %  82.35 % mysqld --defaults-file=/etc/mysql/my.cnf   3021 be/4  root      22.10 M/s    0.00 B/s   0.00 %  15.20 % tar czf /backup/db-20260206.tar.gz /var/lib/mysql    891 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)。两个大读取任务叠加,磁盘带宽被打满了。

3、 用 ionice 调整 IO 优先级

找到了捣乱的进程,如果不能直接 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 优先级
4、 pidstat 补充进程级 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 出场了。

1、 blktrace 工作原理

blktrace 在内核的块层埋了追踪点,记录每个 IO 请求的完整生命周期:

Q (Queued)      → 请求进入块层队列G (Get request) → 分配 request 结构体M (Merged)      → 与已有请求合并I (Inserted)    → 插入调度器队列D (Dispatched)  → 下发到设备驱动C (Completed)   → 设备完成 IO

每个阶段之间的时间差就是该阶段的耗时。Q→C 是总耗时,D→C 是设备实际服务时间,Q→D 是在软件层排队和调度的时间。

2、 blktrace 实战
# 采集 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
3、 btt 统计分析

逐行看 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 很小,说明是调度器或队列配置有问题。

4、 iowatcher 可视化

blktrace 的数据还可以用 iowatcher 生成可视化图表,直观展示 IO 模式:

# 安装 iowatchersudo apt install iowatcher
# 生成 SVG 图表iowatcher -t trace -o io-pattern.svg
# 图表包含:IOPS 时间线、吞吐时间线、IO 延迟分布、IO 偏移量分布(可以看出是顺序还是随机)

六、fio 磁盘性能基准测试

排查 IO 问题时经常需要回答一个基本问题:这块磁盘到底能跑多快?是磁盘本身性能不行,还是应用的 IO 模式有问题?fio 是回答这个问题的标准工具。

1、 fio 核心参数
# 安装 fiosudo apt install fio    # Debian/Ubuntusudo dnf install fio    # RHEL/Fedora

fio 的参数很多,但核心就这几个:

参数
含义
常用值
--rw
IO 模式
read
/write/randread/randwrite/randrw
--bs
块大小
4k
(数据库随机IO)、128k/1m(顺序IO)
--iodepth
队列深度
HDD 用 1-4,SATA SSD 用 32,NVMe 用 64-128
--ioengine
IO 引擎
libaio
(Linux AIO)、io_uring(推荐,内核 6.x)
--numjobs
并发任务数
通常 1-4,测多队列性能时增大
--size
测试文件大小
至少是内存的 2 倍,避免 Page Cache 干扰
--direct
绕过 Page Cache
1
(基准测试必须开启)
--runtime
运行时长
60-120 秒,太短数据不稳定
2、 标准测试场景
# 场景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
3、 fio 输出解读
rand-read: (groupid=0, jobs=4): err= 0: pid=5678  read: IOPS=185.2k, BW=723MiB/s (758MB/s)    slat (nsec): min=1200, max=85000, avg=2850.00, stdev=1200.00    clat (usec): min=45, max=12500, avg=1350.00, stdev=680.00     lat (usec): min=48, max=12520, avg=1353.00, stdev=681.00    clat 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.00  iops        : 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,平均值会掩盖长尾延迟。

4、 io_uring vs libaio

内核 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 倍。

1、 ext4 / xfs / btrfs 对比
特性
ext4
xfs
btrfs
最大文件系统
1 EB
8 EB
16 EB
最大单文件
16 TB
8 EB
16 EB
元数据日志
有序/回写
有序
CoW(无传统日志)
在线扩容
支持
支持
支持
在线缩容
支持
不支持
支持
快照
不支持
不支持
原生支持
透明压缩
不支持
不支持
支持(zstd/lzo)
小文件性能
优秀
良好
一般
大文件顺序写
良好
优秀
良好
并发写入
一般(单 journal)
优秀(延迟分配)
良好
生产稳定性
极高
极高
高(6.x 内核已成熟)

选型建议:

  • 数据库服务器(MySQL/PostgreSQL):xfs。延迟分配和优秀的并发写入性能对数据库友好,RHEL 默认文件系统。

  • 通用 Linux 服务器:ext4。最稳定、最成熟、调优资料最多,Ubuntu 默认文件系统。

  • 需要快照/压缩的场景(日志存储、容器存储):btrfs。透明压缩可以节省 30%-50% 的磁盘空间,快照功能方便备份。

  • Kubernetes 节点:xfs。containerd/CRI-O 的 overlayfs 在 xfs 上表现更好。

2、 文件系统挂载参数调优
# 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 性能调优

1、 readahead 预读调优

预读是内核在检测到顺序读模式时,提前读取后续数据到 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
2、 dirty ratio 脏页参数调优

脏页参数控制 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
3、 队列深度与 nr_requests 调优
# 查看块设备队列深度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,省去合并检查的开销
4、 cgroup v2 IO 限制

在多租户或容器环境下,用 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
案例二:ext4 journal 写放大导致写入性能骤降

现象:一台跑 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 SSD %util 100% 但实际远未饱和

现象:监控告警 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 延迟分布,是定位长尾延迟问题的利器。

1、 biolatency: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 微秒完成,但有少量请求到了毫秒级——这就是长尾延迟
2、 自定义 bpftrace 脚本:按进程追踪 IO 延迟

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 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@    |# ...
3、 biosnoop:逐条追踪 IO 请求

当需要看每一条 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'
4、 ext4slower / xfs_slower:文件系统级慢 IO 追踪

有时候块设备层延迟正常,但文件系统层有额外开销(锁竞争、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 排查流程总结

1、 排查决策树
系统卡顿/业务超时  |  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+)  → 顺序 IO  v深入分析 ──── blktrace+btt 看各阶段延迟  |            bpftrace 看延迟分布和长尾  |            fio 做基准对比  v调优/解决 ──── 调度器/readahead/dirty ratio/cgroup 限制/硬件升级
2、工具速查表
工具
用途
关键命令
开销
top
看 iowait 占比
top -d 1
,按 1 展开核心
极低
iostat
磁盘整体 IOPS/延迟/吞吐
iostat -xmt 1
极低
sar
IO 历史趋势回溯
sar -dp -s 02:00 -e 04:00
无(读历史数据)
iotop
定位 IO 密集进程
sudo iotop -oP
低
pidstat
进程级 IO 统计(可脚本化)
pidstat -d 1 10
极低
ionice
调整进程 IO 优先级
ionice -c 3 -p <PID>
无
blktrace
块层 IO 请求全生命周期追踪
blktrace -d /dev/sda -w 10
中(生成大量数据)
btt
blktrace 数据统计分析
btt -i trace.bin
无(离线分析)
fio
磁盘基准性能测试
见第六章各场景命令
高(压测工具)
biolatency
IO 延迟直方图分布
sudo biolatency-bpfcc -D 1
低
biosnoop
逐条 IO 请求追踪
sudo biosnoop-bpfcc -d sda
中
ext4slower
文件系统级慢操作追踪
sudo ext4slower-bpfcc 1
低
3、 调优参数速查
调优项
参数/文件
推荐值
适用场景
IO 调度器
/sys/block/*/queue/scheduler
NVMe: none,HDD: mq-deadline
所有场景
预读大小
blockdev --setra
顺序读: 2048+,随机读: 64
Kafka/HDFS vs 数据库
脏页硬上限
vm.dirty_ratio
数据库: 5,日志: 40
写入密集场景
脏页软上限
vm.dirty_background_ratio
数据库: 2,日志: 20
写入密集场景
脏页过期
vm.dirty_expire_centisecs
数据库: 1000,日志: 6000
写入密集场景
队列深度
/sys/block/*/queue/nr_requests
高并发: 1024,默认: 256
高 IOPS 场景
IO 合并
/sys/block/*/queue/nomerges
随机 IO: 2,顺序 IO: 0
纯随机 IO 场景
文件系统
mount options
noatime
 必开
所有场景

十二、总结

1、 技术要点回顾

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 争抢问题。

2、进阶学习方向

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 章节覆盖了从原理到工具的完整知识体系

来源丨公众号:马哥Linux运维(ID:magedu-Linux)
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:[email protected]