监控大屏上 iowait 突然飙到 80%,SSH 连上去敲个 ls 要等 5 秒才有响应,业务日志疯狂报超时,数据库慢查询告警刷屏。这种场景在 SRE 的日常里出现频率极高,尤其是跑着 MySQL、Elasticsearch、Kafka 这类重 IO 业务的机器上。CPU 看着不高,内存也没爆,但系统就是卡得像被冻住了一样——十有八九是磁盘 IO 出了问题。
磁盘 IO 问题的棘手之处在于,它不像 CPU 打满那样一眼就能看出来。IO 瓶颈可能藏在文件系统层、Block 层、设备驱动层,甚至是 RAID 卡的缓存策略里。一个 %util 100% 到底意味着什么?await 高是磁盘慢还是队列深?svctm 这个指标还能不能信?这些问题如果搞不清楚,排查就会走弯路。在 云栈社区 里,关于 IO 问题的讨论也总是焦点,毕竟这是后端稳定性绕不开的一环。
这篇文章从 Linux IO 栈的全貌讲起,把 iostat、iotop、blktrace、fio、bpftrace 这套工具链串起来,覆盖从"发现 IO 问题"到"定位根因"再到"调优解决"的完整链路。
一、概述
1.1 背景介绍
1.2 技术特点
- 全栈视角:从 VFS 到块设备驱动,逐层拆解 IO 路径,不停留在工具表面
- 工具链完整:iostat 看全局、iotop 定进程、blktrace 追请求、fio 做基准、bpftrace 抓延迟
- 面向实战:每个工具都给出真实场景下的输出解读,不是照搬 man page
- 调优闭环:不只是发现问题,还覆盖 readahead、dirty ratio、调度器等内核参数调优
1.3 适用场景
- 场景一:生产环境突发 IO 飙升,系统响应变慢甚至卡死,需要快速定位是哪个进程在疯狂读写
- 场景二:数据库服务器 IO 延迟周期性升高,需要区分是磁盘性能不足还是应用层 IO 模式有问题
- 场景三:新机器上线前需要做磁盘性能基准测试,评估 SSD/HDD 是否满足业务 IOPS 和吞吐需求
- 场景四:容器环境下多个 Pod 共享磁盘,需要找出 IO 资源的争抢源头
1.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 的输出就只是在看数字。
2.1 IO 栈分层架构
应用程序 (read/write/pread/pwrite/io_uring)
|
v
VFS (Virtual File System) --- Page Cache
|
v
文件系统 (ext4 / xfs / btrfs)
|
v
Block 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.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 使用 none
ACTION=="add|change", KERNEL=="nvme[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 使用 mq-deadline
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"
EOF
# 重新加载 udev 规则
udevadm control --reload-rules && udevadm trigger
2.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,这远远不够。
3.1 iostat 基础用法
# 最常用的命令:每秒刷新一次,显示扩展信息,单位用 MB
# -x 扩展模式 -m 单位MB -t 显示时间戳 1 间隔1秒
iostat -xmt 1
# 只看特定磁盘
iostat -xmt -d nvme0n1 sda 1
# 看第一次输出要注意:第一行是系统启动以来的累计平均值,不是实时数据
# 从第二行开始才是每秒的实时数据,排查问题时忽略第一行
3.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 %util
sda 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.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 模式
3.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 来定位到具体进程。
4.1 iotop 基础用法
# 需要 root 权限(依赖内核的 taskstats 接口)
# -o 只显示有 IO 活动的进程(过滤掉空闲的)
# -P 显示进程而不是线程
# -a 累积模式(显示自启动以来的累计 IO 量)
sudo iotop -oP
# 推荐使用 iotop-c(C 语言重写版,性能更好)
# Ubuntu: sudo apt install iotop-c
sudo iotop-c -oP
4.2 iotop 输出解读
Total DISK READ: 125.50 M/s | Total DISK WRITE: 42.30 M/s
Actual 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)。两个大读取任务叠加,磁盘带宽被打满了。
4.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.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 出场了。
5.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 是在软件层排队和调度的时间。
5.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
5.3 btt 统计分析
逐行看 blkparse 输出不现实,btt 工具可以自动统计各阶段的延迟分布:
# 用 btt 分析(需要先用 blkparse 生成二进制格式)
blkparse -i trace -d trace.bin
btt -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 很小,说明是调度器或队列配置有问题。
5.4 iowatcher 可视化
blktrace 的数据还可以用 iowatcher 生成可视化图表,直观展示 IO 模式:
# 安装 iowatcher
sudo apt install iowatcher
# 生成 SVG 图表
iowatcher -t trace -o io-pattern.svg
# 图表包含:IOPS 时间线、吞吐时间线、IO 延迟分布、IO 偏移量分布(可以看出是顺序还是随机)
六、fio 磁盘性能基准测试
排查 IO 问题时经常需要回答一个基本问题:这块磁盘到底能跑多快?是磁盘本身性能不行,还是应用的 IO 模式有问题?fio 是回答这个问题的标准工具。
6.1 fio 核心参数
# 安装 fio
sudo apt install fio # Debian/Ubuntu
sudo 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 秒,太短数据不稳定 |
6.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
6.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,平均值会掩盖长尾延迟
6.4 io_uring vs libaio
内核 6.x 环境下强烈推荐使用 io_uring 引擎替代传统的 libaio:
# 对比测试:同样参数,只换引擎
# libaio
fio --name=aio-test --ioengine=libaio --rw=randread \
--bs=4k --iodepth=128 --numjobs=1 --size=4G \
--direct=1 --runtime=30 --directory=/mnt/test
# io_uring
fio --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 倍。
7.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 上表现更好
7.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 性能调优
8.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
8.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=5
sysctl -w vm.dirty_background_ratio=2
sysctl -w vm.dirty_expire_centisecs=1000
sysctl -w vm.dirty_writeback_centisecs=100
# 日志/流式写入服务器(吞吐优先,允许更多脏页缓冲)
sysctl -w vm.dirty_ratio=40
sysctl -w vm.dirty_background_ratio=20
sysctl -w vm.dirty_expire_centisecs=6000
sysctl -w vm.dirty_writeback_centisecs=500
# 持久化到 /etc/sysctl.d/60-io-tuning.conf
cat > /etc/sysctl.d/60-io-tuning.conf << 'EOF'
vm.dirty_ratio = 5
vm.dirty_background_ratio = 2
vm.dirty_expire_centisecs = 1000
vm.dirty_writeback_centisecs = 100
EOF
sysctl --system
8.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,省去合并检查的开销
8.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-jobs
echo "+io" > /sys/fs/cgroup/backup-jobs/cgroup.subtree_control
# 限制 sda 上的读写带宽为 50MB/s,IOPS 为 1000
echo "8:0 rbps=52428800 wbps=52428800 riops=1000 wiops=1000" \
> /sys/fs/cgroup/backup-jobs/io.max
# 把备份进程加入这个 cgroup
echo $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 问题排查案例
9.1 案例一:凌晨定时任务导致数据库延迟飙升
现象:每天凌晨 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
9.2 案例二: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-trace
blkparse -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,否则断电可能丢数据
9.3 案例三: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: DiskIOHighLatency
expr: |
rate(node_disk_io_time_weighted_seconds_total[5m])
/ rate(node_disk_io_time_seconds_total[5m]) > 0.005
for: 5m
labels:
severity: warning
annotations:
summary: "磁盘 IO 平均延迟超过 5ms"
十、bpftrace 追踪 IO 延迟
iostat 给的是平均值,blktrace 数据量太大不适合长期运行。bpftrace 可以用极低的开销实时追踪 IO 延迟分布,是定位长尾延迟问题的利器。
10.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 微秒完成,但有少量请求到了毫秒级——这就是长尾延迟
10.2 自定义 bpftrace 脚本:按进程追踪 IO 延迟
biolatency 只能看全局分布,如果要按进程区分,需要自己写 bpftrace 脚本:
# 保存为 io-latency-by-process.bt
sudo 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 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ |
# ...
10.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 的慢 IO
sudo biosnoop-bpfcc -d nvme0n1 -Q | awk '$NF > 1.0'
10.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 排查流程总结
11.1 排查决策树
系统卡顿/业务超时
|
v
top 看 %wa (iowait) ──── 低 → 不是 IO 问题,排查 CPU/内存/网络
|
高
v
iostat -xmt 1 ──── await 正常 → 可能是应用层阻塞,不是磁盘瓶颈
|
await 高
v
判断设备类型 ──── NVMe → 看 await + IOPS,忽略 %util
| HDD → %util + await + aqu-sz 综合判断
v
iotop -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 限制/硬件升级
11.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 |
低 |
11.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 必开 |
所有场景 |
十二、总结
12.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 多队列架构下完全失真
工具链层面:
- iostat → iotop → blktrace → bpftrace,四级递进:iostat 回答"磁盘整体忙不忙",iotop 回答"谁在读写",blktrace 回答"IO 请求在各阶段花了多少时间",bpftrace 回答"延迟分布长什么样、长尾在哪里"。每个工具解决一个层面的问题,不要指望一个工具搞定所有事
- fio 基准测试是调优的前提:不做基准就调参数等于盲调。先用 fio 的
io_uring 引擎跑出磁盘在 4K 随机读写、128K 顺序读写下的 IOPS 和吞吐上限,再拿业务实际负载的 iostat 数据去对比,才能判断是磁盘能力不足还是应用 IO 模式有问题
调优参数层面:
- 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 争抢问题
12.2 进阶学习方向
-
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 项目是两个值得深入研究的起点,在社区里关于这方面脚本化的讨论也很有价值。
-
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 性能有直接帮助。
-
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 设备的必备技能。
-
存储栈全链路可观测性建设:把 iostat/sar 的指标接入 Prometheus(通过 node_exporter 的 diskstats collector),用 Grafana 构建磁盘 IO 大盘,配合本文提到的告警规则(基于 await 而非 %util),形成从指标采集、异常告警到根因定位的完整闭环。对于 Kubernetes 环境,还需要关注 CSI 驱动层面的 IO 指标和 PV/PVC 级别的 IO 隔离。
12.3 参考资料
- 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 章节覆盖了从原理到工具的完整知识体系