前言
磁盘 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/sw/s:每秒读/写次数(合并后),体现 IOPSrkB/swkB/s:每秒读/写吞吐量%util:设备利用率,持续 >80% 是告警线aqu-sz:平均队列深度,反映积压程度await:平均 I/O 延迟(ms),包含队列等待 + 设备服务时间r_await/w_await:分别统计读、写延迟,有助于区分瓶颈方向
注意:
%util在 NVMe 等支持多队列的设备上会失真。一块 NVMe SSD 的%util可能显示 100%,但实际还有余量。此时应参考await和aqu-sz。
iotop:定位 I/O 热点进程
iostat 告诉你哪块盘忙,iotop 告诉你是谁在读写:
# 仅显示有实际 I/O 的进程,每 2 秒刷新
iotop -o -d 2
# 非交互模式,输出一次后退出
iotop -b -o -n 3
输出中关注 DISK READ 和 DISK 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 SSD →
none:设备本身有硬件队列,软件层排序是多余开销 - SATA SSD →
mq-deadline:兼顾延迟控制和简单性 - HDD →
bfq或mq-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 从库延迟开始追平。
根治措施:
- 对
remark字段加全文索引或将模糊查询改为精确匹配 + 前缀匹配 - 大表 COUNT 操作迁移到离线统计
- 调整 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 带宽都不够。