一、概述
1.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 问题"到"定位根因"再到"调优解决"的完整链路。
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 0200 -e 0400 # 输出示例: # 0201 DEV tps rkB/s wkB/s areq-sz aqu-sz await %util # 0201 sda 1250.00 10000.00 40000.00 40.00 3.50 2.80 92.00 # 0201 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 # 1501 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 案例一:凌晨定时任务导致数据库延迟飙升
现象:每天凌晨 230,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 '
tracepointblock_rq_issue
{
@start[args->dev, args->sector] = nsecs;
@comm[args->dev, args->sector] = comm;
}
tracepointblock_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 # 1501 mysqld 12847 R 16384 1024 2.35 ibdata1 # 1501 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 |
无 |
| 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 争抢问题
六、总结
6.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 章节覆盖了从原理到工具的完整知识体系
全部0条评论
快来发表一下你的评论吧 !