前言

磁盘 I/O 往往是系统性能链条中最慢的一环。一次机械磁盘寻道约 10ms,而内存访问仅约 100ns——两者相差 10 万倍。当业务出现延迟抖动、响应变慢,排查方向总会指向 I/O 子系统。从指标体系出发,结合工具实战与生产案例,建立一套可复用的 I/O 诊断方法论。

I/O 性能指标体系

在动手之前,必须搞清楚四个核心指标的含义及其相互关系。

指标单位含义典型参考值
IOPS次/秒每秒完成的 I/O 读写次数HDD ~100,SATA SSD ~10 万,NVMe SSD ~50 万+
吞吐量MB/s每秒传输的数据量HDD ~150 MB/s,SATA SSD ~550 MB/s,NVMe SSD ~3000 MB/s+
延迟ms/μs单次 I/O 从提交到完成的耗时HDD 5-15ms,SSD 0.1-1ms,NVMe 0.02-0.1ms
队列深度等待处理的 I/O 请求数建议值 NVMe 32-256,SSD 8-32

这四个指标之间存在关键约束关系:

  • 小块随机读写场景下,瓶颈是 IOPS(如数据库 OLTP 4KB 随机写)
  • 大块顺序读写场景下,瓶颈是吞吐量(如日志追加、视频流媒体)
  • 延迟是最终用户感知的指标,即使 IOPS 和吞吐量充足,单次延迟过高仍会导致卡顿
  • 队列深度提升能增加并发,但也意味着单请求等待时间变长

一个重要认知:IOPS × 块大小 ≈ 吞吐量。例如 4KB 块、100 IOPS,吞吐量约 0.4 MB/s;1MB 块、100 IOPS,吞吐量约 100 MB/s。理解这个公式有助于判断瓶颈类型。

诊断工具实战

iostat:宏观 I/O 概览

iostat 来自 sysstat 包,是 I/O 诊断的第一站:

# 安装
yum install -y sysstat        # RHEL/CentOS
apt install -y sysstat        # Debian/Ubuntu

# 查看 所有设备 的扩展统计,每秒刷新,共 5 次
iostat -dxm 1 5

关键输出字段解读:

Device  r/s    w/s   rkB/s  wkB/s  rrqm/s  wrqm/s  %util  aqu-sz  await  r_await  w_await
sda     125.30 38.20 5012.0 1528.0  8.50     2.10    98.70   15.32   45.20   32.10    88.40
  • r/s w/s:每秒读/写次数(合并后),体现 IOPS
  • rkB/s wkB/s:每秒读/写吞吐量
  • %util:设备利用率,持续 >80% 是告警线
  • aqu-sz:平均队列深度,反映积压程度
  • await:平均 I/O 延迟(ms),包含队列等待 + 设备服务时间
  • r_await / w_await:分别统计读、写延迟,有助于区分瓶颈方向

注意%util 在 NVMe 等支持多队列的设备上会失真。一块 NVMe SSD 的 %util 可能显示 100%,但实际还有余量。此时应参考 awaitaqu-sz

iotop:定位 I/O 热点进程

iostat 告诉你哪块盘忙,iotop 告诉你是谁在读写:

# 仅显示有实际 I/O 的进程,每 2 秒刷新
iotop -o -d 2

# 非交互模式,输出一次后退出
iotop -b -o -n 3

输出中关注 DISK READDISK WRITE 两列,快速锁定产生大量 I/O 的进程。如果 iotop 不可用,可从 /proc 直接提取:

# 查看各进程的 I/O 统计(单位:字节)
cat /proc/diskstats | head
for pid in $(ls /proc | grep -E '^[0-9]+$'); do
    io=$(cat /proc/$pid/io 2>/dev/null | grep "write_bytes" | awk '{print $2}')
    [ -n "$io" ] && [ "$io" -gt 0 ] && echo "PID=$pid WRITE_BYTES=$io CMD=$(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ')"
done | sort -t= -k3 -rn | head -10

fio:基准测试

fio(Flexible I/O Tester)是 I/O 性能测试的行业标杆。以下是覆盖常见场景的测试配置:

# fio_test.fio - 磁盘基准测试配置
[global]
ioengine=libaio
direct=1
runtime=60
time_based=1
group_reporting=1
directory=/mnt/testdir
filename=fio_testfile

# 测试1: 4KB 随机读 - 模拟数据库 OLTP
[randread-4k]
bs=4k
rw=randread
iodepth=32
numjobs=4
name=randread-4k

# 测试2: 4KB 随机写 - 模拟数据库写入
[randwrite-4k]
bs=4k
rw=randwrite
iodepth=32
numjobs=4
name=randwrite-4k

# 测试3: 1MB 顺序读 - 模拟大文件读取
[seqread-1m]
bs=1m
rw=read
iodepth=8
numjobs=1
name=seqread-1m

# 测试4: 混合读写 70/30 - 模拟真实业务负载
[mixed-rw]
bs=4k
rw=randrw
rwmixread=70
iodepth=32
numjobs=4
name=mixed-rw

执行测试:

# 创建测试目录
mkdir -p /mnt/testdir

# 运行测试
fio fio_test.fio

# 单项快速测试(命令行模式)
fio --name=randread --ioengine=libaio --direct=1 --bs=4k --rw=randread --iodepth=32 --runtime=30 --time_based --filename=/dev/sdb

# 重点关注的输出指标
#   IOPS    - 每秒 I/O 次数
#   BW      - 带宽(吞吐量)
#   lat     - 延迟分布(avg/min/max/p99)

警告direct=1 绕过 page cache 测试裸盘性能,生产环境中务必使用测试分区或临时文件,避免直接对业务数据盘写入造成数据损坏。

I/O 调度器对比与选择

I/O 调度器负责对块层提交的 I/O 请求进行排序和合并,不同调度器针对不同场景优化。

调度器核心策略适用场景
none(原 noop)不排序,简单合并相邻请求NVMe SSD、无寻道开销设备
deadline读/写分别队列,设截止时间防饥饿通用 SSD、数据库服务器
cfq(完全公平队列)按进程分配 I/O 带宽桌面、多租户混合负载
bfq基于权重公平分配,低延迟桌面、交互式应用

查看与切换调度器:

# 查看当前设备的调度器
cat /sys/block/sda/queue/scheduler
# 输出示例: [mq-deadline] kyber bfq none

# 切换调度器(立即生效,重启失效)
echo bfq > /sys/block/sda/queue/scheduler

# 永久生效:通过 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"
# 机械硬盘使用 bfq
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="bfq"
EOF

选择策略总结

  • NVMe SSDnone:设备本身有硬件队列,软件层排序是多余开销
  • SATA SSDmq-deadline:兼顾延迟控制和简单性
  • HDDbfqmq-deadline:需要减少寻道、防止饥饿
  • 虚拟机磁盘none:宿主机层已有调度,虚拟机内再调度是重复劳动

SSD 与 HDD 优化差异

SSD 专项优化

1. 启用 TRIM

TRIM 告知 SSD 哪些数据块已删除,使其内部垃圾回收更高效,直接影响写入性能的长期稳定性。

# 检查是否支持 TRIM
lsblk -D
# 输出中 DISC-GRAN 和 DISC-MAX 不为 0 即支持

# 手动执行 TRIM(一次性回收所有未使用块)
fstrim -v /
# 输出示例: /: 1234567890 bytes were trimmed

# 配置定时 TRIM(systemd timer,推荐)
systemctl enable --now fstrim.timer
# 默认每周执行一次,查看配置
cat /usr/lib/systemd/system/fstrim.timer

不要在 RAID 阵列的成员盘上直接 fstrim,应使用阵列卡支持的 discard 功能或对逻辑卷执行。

2. 挂载参数优化

# /etc/fstab
UUID=xxx  /data  ext4  defaults,noatime,discard  0 2

# noatime: 不更新文件访问时间,减少写放大
# discard: 启用在线 TRIM(持续模式,对性能有微小影响)
#   - NVMe 建议用定时 fstrim 替代 discard
#   - SATA SSD 可二选一

HDD 专项优化

# 确认 rotational 标志(1=机械盘, 0=SSD)
cat /sys/block/sda/queue/rotational

# 关闭预读对随机读写场景可能有害,但顺序读场景可提升
# 查看当前预读值
blockdev --getra /dev/sda
# 设置预读为 8MB(提升大文件顺序读)
blockdev --setra 8192 /dev/sda

生产案例:高 I/O wait 排查全过程

现象发现

某 MySQL 从库延迟告警,SSH 登录后感觉明显卡顿。执行 top 发现 %wa 高达 60-80%:

top - 14:32:01 up 45 days
%Cpu(s):  5.2 us,  3.1 sy,  0.0 ni, 18.5 id, 72.8 wa,  0.0 hi,  0.4 si

%wa(iowait)表示 CPU 等待 I/O 完成的时间占比。iowait 高不一定代表磁盘慢——如果 CPU 空闲且在等 I/O,iowait 也会升高。需要进一步确认是 I/O 慢还是 CPU 太闲。

定位瓶颈

第一步:iostat 确认设备层面

iostat -dxm 1

关键发现:

Device  r/s     w/s    rkB/s  wkB/s   %util  aqu-sz  await
sda     5230.0  1850.0 20920  7400    100.00  88.45   13.28

%util=100%aqu-sz=88(队列深度极高),await=13ms——磁盘已经满载且积压严重。IOPS 高达 7000+,这对一块 SATA SSD 已接近极限。

第二步:iotop 定位进程

iotop -o -d 2

发现一个 mysqld 进程的 DISK READ 持续在 200+ MB/s,远超正常业务量。

第三步:MySQL 层面分析

-- 查看当前正在执行的查询
SHOW FULL PROCESSLIST;

-- 查看正在进行的大查询
SELECT id, user, host, time, state, LEFT(info, 200) AS query
FROM information_schema.processlist
WHERE time > 10 AND info IS NOT NULL
ORDER BY time DESC;

发现一个全表扫描查询正在跑:

SELECT COUNT(*) FROM order_log WHERE remark LIKE '%keyword%';

order_log 是 200GB 的大表,remark 字段无索引,LIKE '%keyword%' 无法走索引,触发全表扫描。

解决过程

立即止血

-- 终止问题查询
KILL QUERY 12345;

I/O wait 从 72% 降至 5% 以内,MySQL 从库延迟开始追平。

根治措施

  1. remark 字段加全文索引或将模糊查询改为精确匹配 + 前缀匹配
  2. 大表 COUNT 操作迁移到离线统计
  3. 调整 InnoDB I/O 相关参数,降低单次 I/O 压力:
# my.cnf - InnoDB I/O 优化
[mysqld]
# 刷脏页的并发数,SSD 可设为 4-8
innodb_io_capacity = 2000
innodb_io_capacity_max = 4000
# 读预读窗口,SSD 可适当降低
innodb_read_io_threads = 8
innodb_write_io_threads = 8

排查方法论总结

top (%wa 高)
  → iostat (确认 %util / await / aqu-sz)
    → iotop (定位进程)
      → 进程层面分析 (MySQL/应用日志)
        → 根因: SQL/代码/配置
          → 止血 + 根治

核心原则:不要看到 iowait 高就调磁盘参数,先确认 I/O 压力是否合理——如果是不合理的全表扫描,加多少 I/O 带宽都不够。

参考资料