磁盘是四类系统资源里最慢的一类:内存访问是纳秒级,SSD 随机读是几十到几百微秒,机械盘随机读要几毫秒到十几毫秒。差距是几千到几万倍。所以一旦业务开始绕不开磁盘,延迟就会肉眼可见地劣化,而且症状往往表现为"CPU 在等"或者"Load 很高但 CPU 很闲",很容易被误判。
这篇文章解决磁盘 IO 的定位和处置:iostat -x 每个字段的确切含义、%util 为什么在现代设备上会骗人、iotop 和 pidstat -d 怎么找到具体进程、怎么知道进程到底在读写哪些文件、六类常见成因的特征、以及从挂载参数到调度器到 cgroup 限流的完整手段清单。
一、先确认是不是磁盘的问题
磁盘 IO 问题有两个非常典型的先行症状,看到其中一个就该去查 IO:CPU 的 wa(iowait)明显升高,意味着 CPU 闲着但被 IO 阻塞的任务占着时间表;以及 Load 很高但 CPU 很闲,意味着大量进程卡在不可中断的 D 状态。
top # 看 %wa
vmstat 1 # 看 b 列(不可中断进程数)和 wa
但要注意 wa 的陷阱:iowait 高只说明"CPU 在空闲且有任务在等 IO",不代表磁盘一定忙。 如果只有一个进程在做同步 IO(比如单线程写入日志),CPU 大部分时间就是空闲的,wa 会很高,但磁盘吞吐量其实很低,并没有接近硬件上限。所以判断磁盘是不是瓶颈,必须结合下面的设备级指标。
另一条有用的确认路径是直接对比:把业务的响应时间和设备的 await 放在一起看。磁盘响应变长的同时业务 P95 也变长,才有可能是磁盘造成的。
二、iostat -x 1 全部字段怎么读
iostat -x 1 # 每秒刷新,扩展统计
iostat -x 1 5 # 刷新 5 次后退出
iostat -x -p ALL 1 # 含每个分区
iostat -xz 1 # -z 跳过无活动的设备,输出更干净
iostat -d -m 1 # 只要吞吐,MB 单位
典型输出:
Device r/s w/s rkB/s wkB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sda 12.00 245.00 96.00 51200.00 0.00 12.00 0.00 4.67 1.20 18.60 4.82 8.00 208.98 3.60 92.40
逐列解释(按重要性排序):
| 字段 | 含义 | 怎么用 |
|---|---|---|
| %util | 设备有 IO 请求在处理的时间占比 | 粗略忙碌程度,SSD 上参考价值下降,见下文 |
| r_await / w_await | 读/写请求平均耗时(毫秒),含排队和服务时间 | 核心判据,对比设备基线 |
| await | 读写合并后的平均耗时(现已按 r/w 拆分开) | 综合等待时间 |
| aqu-sz (avgqu-sz) | 平均请求队列长度 | 持续 > 1 说明开始堆积 |
| r/s + w/s | 每秒读/写请求数,即 IOPS | 对比设备能力 |
| rkB/s + wkB/s | 每秒读写吞吐量 | 对比带宽上限 |
| rareq-sz / wareq-sz | 平均请求大小(KB) | 判断是随机小 IO 还是顺序大 IO |
| rrqm/s / wrqm/s | 每秒被合并的请求数 | 合并率高说明相邻地址、调度器起了作用 |
| svctm | 已废弃,不要采信 | sysstat 手册明确说明该字段不可靠 |
关于 svctm 必须单独强调:这个字段曾经表示"设备处理单个请求的平均服务时间",但在现代 sysstat 版本中它无法从内核 blk-mq 导出的数据里正确推导,对多队列(blk-mq)设备完全不可靠,官方手册明确写了 "svctm is no longer meaningful and will be removed"。判断服务质量请用 await 而不是 svctm。
%util 为什么会骗人
%util 的计算方式是"设备队列非空的时间 / 总时间"。这个算法隐含一个假设:设备一次只能处理一个请求。对单队列的机械盘这个假设基本成立,所以 %util 接近 100% 时确实意味着设备饱和。
但对 NVMe SSD、支持 NCQ 的 SATA SSD、以及 RAID 阵列来说,设备内部可以并行处理几十上百个请求。这时候可能出现:%util 已经是 100%,但 await 只有 1ms,吞吐也远没到上限——设备一直在"有请求在处理",但它同时在处理很多个,完全没有吃力。
所以正确的判断顺序是:
- 先看 await,和设备正常水平对比(见下方经验值)。
- 再看 avgqu-sz,持续大于 1 说明请求开始排队。
- 最后才看
%util,而且只在机械盘上赋予它"饱和"的含义。
await 的经验参考(随机读写场景,具体因设备而异):
| 设备类型 | 正常 await | 需要警惕 |
|---|---|---|
| NVMe SSD | 0.1 ~ 1 ms | > 3~5 ms |
| SATA SSD | 0.2 ~ 3 ms | > 10 ms |
| 企业级机械盘 | 5 ~ 15 ms | > 30~50 ms |
| 网络存储(NFS/iSCSI/云盘) | 1 ~ 10 ms | > 20 ms |
这些数字是经验值,不是标准。 正确做法是采集自己设备在健康状态、不同负载下的 await 作为基线,然后对比。云端块存储还有一个特点:很多云盘的性能(IOPS 和吞吐)与容量或实例规格挂钩,达到配额上限后 await 会明显跳升,这时候要看云厂商提供的监控指标确认是否撞到了配额。
另一个容易忽略的场景:写被内存挡住了
iostat 看到的是"到达块设备层"的 IO,不包含被 Page Cache 吸收的部分。所以进程写数据很慢时,iostat 可能显示设备很闲,因为瓶颈在回写阶段:脏页积累到一定比例后内核开始强制回写,此时进程会被阻塞。这时候要看:
watch -n 1 'grep -E "^(Dirty|Writeback)" /proc/meminfo'
# Dirty 持续很大(比如超过总内存的 10%)说明回写压力大
sar -B 1 # pgpgout/s、majflt/s
三、用 iotop 和 pidstat 定位到进程
设备指标确认有问题之后,下一步是找进程。
sudo iotop # 交互式,默认按 IO 带宽排序
sudo iotop -o # 只显示真正在做 IO 的进程(最常用)
sudo iotop -o -P # 只显示进程,不展开线程
sudo iotop -a # 累计模式(自启动以来总量),适合找"总量大户"
sudo iotop -p PID # 只看指定进程
交互按键:o 切换只显示活动进程,a 切换累计/实时模式,r 反向排序,左右箭头切换排序字段。
需要装:
sudo dnf install -y iotop sysstat # RHEL、Rocky、CentOS
sudo apt install -y iotop sysstat # Debian、Ubuntu
iotop 的输出各列含义:DISK READ/DISK WRITE 是该进程真正落到块设备层的吞吐(不含被缓存吸收的部分),SWAPIN 是换页占比,IO> 是该进程做 IO 等待的时间占比——IO> 列很高说明进程被 IO 阻塞,比吞吐列更能反映"谁在等 IO"。
pidstat -d 是脚本友好、可持续采样的替代:
pidstat -d 1 5 # 每秒一次共 5 次
pidstat -d 1 # 持续
pidstat -dt -p PID 1 # -t 展开线程,针对多线程 IO
输出里 kB_rd/s、kB_wr/s 是读写吞吐,iodelay 是被阻塞的时长(时钟滴答数)。iodelay 列很关键:吞吐量不大但 iodelay 很高,说明进程虽然写得不多却一直被卡住,通常是在等 fsync 或者遇到了慢设备。
两者的小陷阱:iotop 需要内核开启相关统计(CONFIG_TASKSTATS、CONFIG_TASK_DELAY_ACCT),某些精简内核或旧版本上部分列会显示为 0。另外它依赖 root 权限读取每个进程的 /proc/PID/io。
四、知道进程在读写什么
找到进程之后,还有最后一公里:它在动哪些文件。这决定了你能做什么。
# 进程累计 IO 统计(自启动以来总读写字节数、被取消的写等)
sudo cat /proc/PID/io
# rchar/wchar 是系统调用层面的读写字节(含被缓存的)
# read_bytes/write_bytes 是真正到块设备的字节数
# cancelled_write_bytes 是被截断或覆盖而没真正落盘的写字节
# 该进程打开的文件
sudo ls -l /proc/PID/fd | head -40
sudo lsof -p PID | grep -vE 'IPv4|IPv6|unix' | head -40
# 只看正在写的文件(fd 偏移在增长)
sudo lsof -p PID | awk '$5=="w" || $5=="u"'
# 某目录下被打开的文件
sudo lsof +D /var/log | head -30
# 动态追踪系统调用(高开销,仅限短时使用)
sudo strace -p PID -f -e trace=write,read,openat,fsync,fdatasync -s 200 -c
# -c 输出统计汇总,能直接显示哪个系统调用最频繁、耗时最多
strace 的风险提示:它会让被追踪进程每次系统调用都停下来,-f 还会追踪所有子进程。在高 IO 进程上开销可能是倍数级的,可能导致业务超时加剧。生产环境原则:限时 5~10 秒、优先用 -c 只要统计不要每条输出、做好随时 Ctrl+C 的准备。更轻量的选择是 perf trace(基于 perf 事件,开销小得多):
sudo perf trace -p PID -s # -s 输出摘要,系统调用名+次数+耗时
sudo perf trace -p PID --duration 5 -e write,fsync
常见的定位结论通过这套组合基本能出来:发现就是 mysqld 在狂写 ibdata1 和 redo log(数据库刷盘);发现是 rsyslog/journald 在写 messages 文件(日志无节制);发现是某个 backup 脚本在读整个 data 目录(备份任务);发现三个作用很不明显的进程在写不同的临时文件(可能是木马或失控脚本)。
五、六类常见成因与对策
| 成因 | 特征 | 对策 |
|---|---|---|
| 数据库刷盘 / checkpoint | 周期性 IO 尖峰,写吞吐巨大,w_await 高 | 调整 innodb_io_capacity、innodb_flush_neighbors、redo log 大小;换 SSD |
| 日志无节制写入 | wkB/s 持续高,进程以 rsyslog/应用为主 | 降低日志级别(DEBUG → INFO/WARN)、用异步 appender、给 rate-limit、配置 logrotate |
| 备份/巡检/同步任务 | 时间规律性强(如每天凌晨),读 IO 极高 | 改到低峰期、ionice -c3 降优先级、用增量备份、限制带宽 |
| 内存不足导致 swap | vmstat 的 si/so 不为 0,伴随随机读 |
加内存或降低内存占用,这是伪 IO 问题,根因是内存 |
| 小文件海量随机 IO | IOPS 高但吞吐低,rareq-sz 很小 | 合并小文件、改用对象存储、调整预读参数 |
| 磁盘真的到硬件上限 | await 高、吞吐接近设备规格 | 换更快设备、做 RAID、分散到多块盘 |
降低 IO 影响:ionice 与 cgroup 限流
ionice 适用于 CFQ/BFQ 这类支持优先级的调度器,对 mq-deadline、none(特别是 NVMe 多队列设备)效果有限,但对机械盘和部分场景仍然有用:
sudo ionice -c 3 -p PID # idle 类:仅当无其他 IO 时才让该进程做 IO
sudo ionice -c 2 -n 7 -p PID # best-effort 类的最低优先级
sudo ionice -c 2 -n 0 -p PID # best-effort 最高优先级(给关键业务)
ionice -c 3 rsync -a /data /backup # 启动时直接设置
查看进程当前的 IO 优先级:ionice -p PID。
cgroup v2 的 io.weight / io.max 是现代的通用方案,对所有设备类型都生效:
# systemd 方式限制服务读写带宽(绝对路径必须指向具体设备)
sudo systemctl set-property --runtime backup.service IOReadBandwidthMax="/dev/sdb 20M"
sudo systemctl set-property --runtime backup.service IOWriteBandwidthMax="/dev/sdb 20M"
sudo systemctl set-property --runtime backup.service IOWeight=10
# cgroup v1 的 blkio 写法(旧系统)
echo '8:16 10485760' > /sys/fs/cgroup/blkio/backup/blkio.throttle.write_bps_device
# 8:16 是主:次设备号,用 lsblk -d -o NAME,MAJ:MIN 查看
查看设备号:
lsblk -d -o NAME,MAJ:MIN,SIZE,ROTA,MODEL
# ROTA=1 是机械盘,ROTA=0 是 SSD
六、优化手段清单
按"实施成本 vs 收益"排序,从上往下做:
挂载参数:noatime。 默认挂载时每次读文件都会更新 atime,产生额外的写 IO。对于读多写少的场景收益明显。
# 查看当前挂载参数
mount | grep -E ' / | /data'
# 临时重新挂载
sudo mount -o remount,noatime /data
# 永久生效:编辑 /etc/fstab,在 options 列加上 noatime(保留原有参数)
# UUID=xxx /data ext4 defaults,noatime 0 2
sudo mount -o remount /data # 测试新参数能否生效
sudo findmnt --verify # 检查 fstab 语法(部分版本支持)
风险提示:改 /etc/fstab 写错会导致系统无法启动。修改前备份 cp /etc/fstab /etc/fstab.bak.$(date +%F),改完必须用 mount -o remount(或直接 mount -a)验证,不要在不确定时能重启。另外数据库管理员有时会依赖 atime(极少见),确认后再改。
IO 调度器。 不同设备的选择完全不同:
# 查看当前调度器(方括号内是生效的)
cat /sys/block/sda/queue/scheduler
# 常见输出:none [mq-deadline] kyber bfq
# 临时切换
echo mq-deadline | sudo tee /sys/block/sda/queue/scheduler
# 永久生效:用 udev 规则或 systemd-tmpfiles,推荐 udev
cat <<'EOF' | sudo tee /etc/udev/rules.d/60-ioscheduler.rules
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="none"
ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="mq-deadline"
EOF
sudo udevadm control --reload-rules && sudo udevadm trigger
| 调度器 | 适用设备 | 特点 |
|---|---|---|
| none(noop) | NVMe SSD、高端 RAID(有硬件调度) | 只做简单合并,把决策交给硬件,开销最小 |
| mq-deadline | SATA SSD、机械盘、多数通用场景 | 保证请求的截止时间,读优先于写,默认优选 |
| kyber | 快速设备(SSD) | 按延迟目标调节队列深度 |
| bfq | 桌面、需要公平性的多任务场景 | 按进程公平分配带宽,吞吐略低 |
注意:只有 rotational 设备才有必要保留 mq-deadline 的合并逻辑;NVMe 设备用 none 通常吞吐更高。但实际差别往往不大,不要期待换调度器带来数量级提升,这属于最后一类优化。
分离日志盘与数据盘。 数据库的数据文件(随机 IO)和 redo/binlog(顺序写)放在不同的物理设备上,能显著降低互相干扰。这是结构性的收益,比调参有效得多。
提高内存间接减少 IO。 更多内存意味着更大的 Page Cache 和更大的数据库 buffer pool,热数据命中率提升后磁盘 IO 自然下降。这经常是"加内存解决 IO 问题"的合理解释。
数据库侧参数。 以 MySQL 为例:innodb_io_capacity 应设置为设备的实际 IOPS 能力(SSD 通常可以设到几千到上万,机械盘几百),它控制后台刷盘的强度,设低了会导致脏页堆积、突增的 checkpoint IO;innodb_flush_neighbors=0(SSD 上关闭相邻页合并);适当增大 redo log 可以减少强制 checkpoint 的频率。改动前查官方文档对应版本的默认行为,不同版本差异很大。
给日志设置 rotatelog 与速率限制。 rsyslog 支持 $SystemLogRateLimitInterval,systemd-journald 可以通过 /etc/systemd/journald.conf 的 SystemMaxUse、RateLimitIntervalSec、RateLimitBurst 限制写入。
# 查看日志占用
journalctl --disk-usage
sudo du -sh /var/log/*
# journald 配置示例
sudo grep -E 'SystemMaxUse|RateLimit' /etc/systemd/journald.conf
sudo systemctl restart systemd-journald # 会短暂中断日志收集
七、把 IO 指标纳入日常监控
单次排查解决当下问题,长期要靠监控。建议采集的最小集合:
| 指标 | 来源 | 告警建议 |
|---|---|---|
| await(读/写分开) | iostat -x 1 / node_exporter |
超过自身基线 2 倍,持续 3 分钟 |
| aqu-sz | iostat -x 1 |
持续大于 CPU 核数的一半 |
| %util(仅机械盘重点看) | iostat -x 1 |
持续 > 90%(机械盘) |
| IOPS / 吞吐 | iostat -d 1 |
接近设备规格 80% |
| 磁盘空间使用率 | df |
> 80% 预警,> 90% 告警 |
| inode 使用率 | df -i |
> 80% 告警(容易被忽略) |
| 介质错误 | dmesg、smartctl |
出现即告警 |
长期采集用 sar -d,历史数据存在 /var/log/sa/saXX(RHEL 系)或 /var/log/sysstat/saXX(Debian 系),这是事后复盘最有价值的数据:
sar -d -f /var/log/sa/sa$(date +%d) # 当天设备 IO 历史
sar -d -p -s 02:00:00 -e 04:00:00 -f /var/log/sa/sa15 # 指定时间段
inode 耗尽是特别值得单独说的一条:磁盘还剩几十 GB 空间但服务已经开始报 "No space left on device",多半是 inode 用完了(海量小文件场景,如邮件队列、session 文件、容器镜像层)。用 df -i 确认。
常见问题
%util 已经是 100% 了,但业务好像不慢,需要管吗?
看 await。如果 await 还处于设备的正常水平(比如 NVMe 上 1ms 左右),那 100% 只是说明设备一直在处理请求,没有排队,属于 SSD 并行度高导致的读数特点,不用处理。只有当 await 明显超出基线、avgqu-sz 持续大于 1 时才是真瓶颈。相反,机械盘上 %util 到 90% 通常已经值得关注了。
iostat 里 svctm 显示很大,是不是设备坏了?
不要采信 svctm。sysstat 官方手册明确说明该字段对现代多队列设备已不可靠并将被移除。判断服务时间请用 await(以及拆分后的 r_await/w_await)。这个字段在很多新版本里已经被标记为弃用。
为什么 iotop 里所有进程的 IO 加起来,比 iostat 看到的设备吞吐少很多?
几个原因:一是 Page Cache 吸收了部分读写,进程看到的 IO 没到块设备层;二是 write-back 机制让写入延后发生,而 iotop 归属到了当时的进程;三是文件系统层本身会产生间接 IO(元数据更新、日志(journal)写入),这部分不会归属到任何用户进程。少量差异正常,差异很大时优先检查是否有 kworker/journal 线程的活动。
该怎么判断是随机 IO 还是顺序 IO?
看 rareq-sz/wareq-sz(平均请求大小,单位 KB)。数值很大(比如几百 KB 以上)且 rrqm/wrqm 合并率高,说明是顺序 IO;数值很小(4~16KB)且 IOPS 高,说明是随机 IO。随机 IO 对机械盘极其不利(要寻道),对 SSD 友好得多。这是判断"换 SSD 有没有用"的关键依据:随机 IO 密集的场景换 SSD 收益巨大,纯顺序大文件传输换 SSD 收益有限(瓶颈可能在带宽)。
磁盘 IO 高的时候,先加内存还是先换 SSD?
先看前面提到的内存 Signal:如果 vmstat 里 si/so 不为 0,说明是在换页,根因是内存不足,加内存才对症。如果没有 swap、await 高、%util 高,是设备能力问题,换 SSD 或分散 IO。如果 await 不高但读写量确实很大且无法减少,也可以考虑加内存扩大缓存来降低读 IO——判断依据是文件系统是否还有足够的缓存空间留给热数据。
ionice 设置了但好像没效果?
检查当前的 IO 调度器。ionice 只对支持优先级/公平性的调度器(CFQ、BFQ)有效,对 mq-deadline 和 none 不生效。NVMe 这类多队列设备默认可能就是 none。这种情况请改用 cgroup v2 的 io.max/io.weight 做带宽限制。
总结
- 磁盘 IO 问题的先行症状是 CPU 的 wa 升高和 Load 高但 CPU 闲,但 wa 本身不能单独作为判据。必须用
iostat -x 1看设备级指标确认。 iostat -x里最该看的是 await(含拆分后的 r_await/w_await)和 avgqu-sz,而不是 %util。svctm 已废弃不要采信。await 参考区间:NVMe 0.1~1ms、SATA SSD 0.2~3ms、机械盘 5~15ms,超出自家设备基线 2 倍基本可判定为瓶颈。%util的含义是"设备队列非空的时间占比",在可并行的 SSD/RAID 设备上会失真——即使 100% 也可能毫无排队。它对单队列机械盘仍有意义。判断口诀:先看 await,再看队列长度,最后才看 %util。- 定位进程用
iotop -o(重点看IO>列,"谁在等 IO"比"谁吞吐大"更关键)或pidstat -d 1(可脚本化、看 iodelay)。想知道在读写哪些文件用lsof -p PID配合/proc/PID/fd,动态追踪优先用开销更小的perf trace -s而不是strace。 - 常见成因按频率排列:数据库刷盘、日志无节制写入、备份同步任务、内存不足导致 swap、海量小文件随机 IO、设备真的到上限。其中 swap 导致的 IO 问题根因在内存,加内存才对症。
- 优化手段从高收益到低收益:挂载
noatime(改 fstab 前必须备份并验证)、选择与设备匹配的调度器(NVMe 用 none、SSD 与机械盘用 mq-deadline)、日志盘与数据盘分离、增加内存扩大缓存、最后才是数据库参数微调。 - 给备份类任务降优先级:机械盘/CFQ/BFQ 场景用
ionice -c3,多队列设备用 cgroup v2 的IOReadBandwidthMax/IOWeight(可通过systemctl set-property设置)。同时把 await、队列长度、IOPS、磁盘空间与 inode 使用率纳入日常告警,inode 耗尽会伪装成"磁盘满了"。
企业QQ咨询




