磁盘IO问题的定位根因与调优解决思路

描述

一、概述

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

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分