iostatsysstat 工具包中的磁盘 I/O 观测命令。它能回答三类常见问题:磁盘当前有多少 IOPS、读写带宽和延迟是多少,以及压力集中在哪块块设备上。

本文重点记录实时采样、添加时间戳、筛选 NVMe 设备、保存和查询日志,以及如何避免把 %util=100% 直接等同于“磁盘已经跑满”。

安装与快速开始

Debian、Ubuntu:

1
sudo apt install sysstat

RHEL、CentOS、Rocky Linux:

1
sudo dnf install sysstat

最常用的实时观测命令是:

1
iostat -x 1
  • -x:显示扩展设备指标;
  • 1:每 1 秒输出一次,直到按 Ctrl+C 停止。

若只采样 5 次:

1
iostat -x 1 5

一条适合排障的命令

1
S_TIME_FORMAT=ISO iostat -d -x -t -y -z nvme0n1 nvme1n1 1
参数 作用
-d 只显示设备报告,不显示 CPU 报告
-x 显示 IOPS、带宽、延迟、队列和利用率等扩展指标
-t 为每组报告打印时间戳
-y 跳过“自系统启动以来”的第一组累计平均值
-z 隐藏采样周期内完全没有活动的设备
nvme0n1 nvme1n1 只查看指定设备
1 每秒持续采样

S_TIME_FORMAT=ISO 让时间戳使用便于排序和查询的 ISO 格式。若系统上的 iostat 不支持某个参数,先用下面的命令核对版本和帮助:

1
2
iostat -V
iostat --help

为什么每秒执行一次,结果却完全相同

iostat 的第一组报告默认不是“当前一秒”,而是从系统启动到现在的平均值;后续报告才表示相邻采样之间的变化。

下面这种循环会反复启动新进程,每次都只取第一组累计值,因此多行结果可能几乎不变:

1
2
3
4
5
# 不推荐
while true; do
date '+%F %T'
iostat -x nvme0n1 nvme1n1 1 1
done

应让同一个 iostat 进程持续采样,并用 -y 丢弃第一组累计值:

1
S_TIME_FORMAT=ISO iostat -x -t -y nvme0n1 nvme1n1 1

如果只需要 60 个有效样本:

1
S_TIME_FORMAT=ISO iostat -x -t -y nvme0n1 nvme1n1 1 60

核心字段怎么看

不同 sysstat 版本显示的列略有差异,排障时优先关注下面这些字段:

字段 含义 判断方式
r/s 每秒完成的读请求数 读 IOPS,已是合并后的块层请求数
w/s 每秒完成的写请求数 写 IOPS,已是合并后的块层请求数
rkB/s 每秒读取的数据量 实际按 KiB/s 计算,尽管表头通常写作 kB/s
wkB/s 每秒写入的数据量 除以 1024 可近似换算为 MiB/s
r_await 读请求平均完成时间 单位毫秒,包括排队和设备处理时间
w_await 写请求平均完成时间 单位毫秒,包括排队和设备处理时间
aqu-sz 平均队列长度 老版本中叫 avgqu-sz
rareq-sz 平均读请求大小 单位 KiB
wareq-sz 平均写请求大小 单位 KiB
%util 采样周期内设备存在 I/O 的时间占比 对串行设备接近 100% 时可能饱和;对 NVMe、RAID 不能单独作为性能上限依据

老版本中的 avgrq-sz 以扇区为单位,新版本的 areq-sz 以 KiB 为单位;avgqu-sz 后来改名为 aqu-sz。分析历史日志时应先保留并确认表头,不能只看没有列名的数据行。

IOPS 怎么算

若只考虑普通读写:

1
总 IOPS ≈ r/s + w/s

例如:

1
r/s=45.32  w/s=579.78

总 IOPS 约为:

1
45.32 + 579.78 = 625.10

这是当前业务实际发给块设备并完成的 IOPS,不是磁盘通过压力测试能够达到的最大 IOPS。

带宽和请求大小怎么算

假设某块盘的数据为:

1
w/s=579.78  wkB/s=12858.13

写带宽约为:

1
12858.13 / 1024 ≈ 12.56 MiB/s

平均每个写请求约为:

1
12858.13 / 579.78 ≈ 22.18 KiB

因此,数据行中的 12858.13 表示写带宽,不是 IOPS;是否正常还要结合业务负载、请求大小和设备基线判断。

%util=100% 是否等于磁盘跑满

不一定。

%util 表示采样周期内,有 I/O 请求被发往设备的时间占比。对一次只能串行处理一个请求的设备,它接近 100% 往往意味着饱和;但 NVMe SSD、RAID 和其他能并行处理请求的设备,即使 %util=100%,仍可能继续提高并发和吞吐。

判断是否存在存储瓶颈,至少要同时观察:

  1. r_awaitw_await 是否相对正常基线持续升高;
  2. aqu-sz 是否持续增长;
  3. IOPS 或带宽是否已经接近该设备在相同块大小、读写模式和队列深度下的实测上限;
  4. 应用延迟是否与上述变化出现在同一时间窗;
  5. CPU、网络、文件系统和应用自身是否存在更早出现的瓶颈。

同理,CPU 的 %iowait 低不能证明磁盘没有瓶颈,%iowait 高也不能单独证明某块磁盘已经饱和。

记录到日志文件

持续记录指定设备:

1
2
S_TIME_FORMAT=ISO iostat -d -x -t -y -z nvme0n1 nvme1n1 1 \
> iostat.log 2>&1

只记录 10 分钟:

1
2
3
timeout 10m env S_TIME_FORMAT=ISO \
iostat -d -x -t -y -z nvme0n1 nvme1n1 1 \
> iostat.log 2>&1

放到后台运行:

1
2
3
nohup env S_TIME_FORMAT=ISO \
iostat -d -x -t -y -z nvme0n1 nvme1n1 1 \
> iostat.log 2>&1 &

iostat 主要读取内核维护的统计信息,通常开销很低;长期运行时更需要关注的是日志文件自身的空间占用。生产环境建议设置采样期限或使用 logrotate 做轮转。

查询历史日志

查询指定时间范围

假设时间戳格式为 YYYY-MM-DD HH:MM:SS,可提取 15:00 到 15:10 的完整报告:

1
2
3
4
5
6
7
8
9
awk -v start='2025-06-09 15:00:00' \
-v end='2025-06-09 15:10:00' '
/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ {
ts = substr($0, 1, 19)
gsub(/T/, " ", ts)
keep = (ts >= start && ts <= end)
}
keep
' iostat.log

ISO 格式按年月日从大到小排列,可以直接进行字符串范围比较。

只看某块设备

1
2
3
4
awk '
/^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/ { ts=$0 }
$1 == "nvme0n1" { print ts; print }
' iostat.log

临时查看时也可以直接使用:

1
grep -E '^[0-9]{4}-|^nvme0n1[[:space:]]' iostat.log

脚本需要稳定消费数据时,可考虑有限次数采样后输出 JSON:

1
iostat -d -x -y -o JSON nvme0n1 1 5 > iostat.json

JSON 字段会随 sysstat 版本增加,不应依赖字段顺序。

ZFS 场景如何配合观察

iostat 看到的是 Linux 块设备层的物理 I/O;zpool iostat 看到的是 ZFS pool 和 vdev 的逻辑 I/O。写合并、副本或 RAID 冗余都可能让两层数值不同,因此排查 ZFS 时应同时观察。

查看底层设备:

1
S_TIME_FORMAT=ISO iostat -d -x -t -y nvme0n1 nvme1n1 1

查看 ZFS pool 和 vdev,并显示日期时间:

1
zpool iostat -v -T d -y 1

同样不要用循环反复执行 zpool iostat -v 1 1:它的第一组报告也默认是系统启动以来的平均值。让命令持续运行,或使用支持的 -y 跳过第一组累计值。

一套实际排障顺序

  1. lsblk 确认真实设备名,不要把控制器名、旧设备名或不存在的别名直接写进采集命令;
  2. 执行 iostat -x -t -y <device> 1,保存带表头的连续样本;
  3. 先对齐应用异常的时间窗,再看 IOPS、带宽、延迟、队列和 %util
  4. 用同一块设备、相同块大小和读写模式的历史数据或 fio 基准作对照;
  5. ZFS 环境再结合 zpool iostat,定位差异发生在 pool、vdev 还是底层设备;
  6. 若设备指标正常,继续检查 CPU、网络、文件系统和应用并发,不要为了“跑满磁盘”而盲目增加压力。

参考资料