磁盘 I/O:IOPS、吞吐与延迟的三角关系
为什么队列深度一加大 IOPS 就上去、延迟也跟着上去?这节课把 iostat 每一列讲透。
学完这节你能做到
- 推导 IOPS × 块大小 = 吞吐,并解释它何时不成立
- 逐列读懂 iostat -x 输出,判断磁盘是否饱和
- 区分 HDD / SATA SSD / NVMe 的性能量级与适用场景
一次 I/O 都经过了什么
应用调用 write() 之后,数据并不会立刻躺在盘片或闪存颗粒上。它要穿过一串组件:
应用 write()
└─ 系统调用 → VFS
└─ 文件系统(XFS / ext4)
└─ 页缓存(脏页,稍后回写)
└─ 块层(bio → request,合并 + 调度)
└─ 设备驱动(NVMe / SCSI)
└─ 设备内部(控制器、缓存、介质)
这条链上每一层都可能成为瓶颈,而且每一层看到的"一次 I/O"都不一样:应用写 4KB,文件系统可能因为日志变成两次写,块层可能把相邻的几个请求合并成一个,SSD 内部还可能因为垃圾回收把它放大成几倍的实际擦写。
默认情况下 write() 只是把数据放进页缓存就返回了,真正落盘由内核后台回写。只有 fsync()(或以 O_DIRECT / O_SYNC 打开)才会等待设备确认。数据库和存储服务大量使用 fsync,所以它们对盘的写延迟极其敏感——这也是存储节点必须选带掉电保护的企业盘的原因。
四个量之间的关系
上一节已经建立了 吞吐 ≈ IOPS × 块大小。现在补上第四个量——并发,它才是解释"为什么延迟和 IOPS 会同时上升"的关键。
排队论里有个 Little's Law,落到存储上可以这么记:
并发数(队列深度) ≈ IOPS × 延迟
举个例子:一块盘单请求延迟 100µs。
- 队列深度 1:每秒最多 1 / 0.0001 = 10000 IOPS,延迟 100µs
- 队列深度 32:设备内部并行处理,IOPS 可以涨到 30 万,但每个请求的延迟会升到 100µs 以上
加大队列深度换来吞吐,代价是延迟。 所以压测报告里只写 IOPS 不写队列深度和延迟,等于什么都没说。
排队系统的响应时间大致正比于 1 / (1 - 利用率)。利用率从 50% 涨到 60%,延迟变化很小;从 90% 涨到 95%,延迟直接翻倍。这就是为什么容量规划要给磁盘留水位——不只是怕写满,更是怕延迟崩掉。
iostat -x 逐列精读
这是存储工程师看得最多的一屏。以 iostat -xz 1 为例:
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 %util
nvme0n1 12043 3210 770752 205440 0.00 1.20 0.00 0.04 0.31 0.45 4.83 64.00 64.00 99.20
逐列拆开:
| 列 | 含义 | 怎么用 |
|---|---|---|
r/s w/s | 每秒读/写次数,即 IOPS | 和业务预期对得上吗 |
rkB/s wkB/s | 每秒读/写数据量 | 除以 IOPS 就是平均块大小 |
rrqm/s wrqm/s | 被块层合并掉的请求数 | 合并率高说明是顺序 I/O |
r_await w_await | 请求平均耗时,含排队时间 | 最重要的一列 |
aqu-sz | 平均队列长度 | 饱和度信号,持续大于 1 说明在排队 |
rareq-sz wareq-sz | 平均请求大小(KB) | 判断随机小 I/O 还是顺序大 I/O |
%util | 有 I/O 在处理的时间占比 | 对 NVMe 已失真,仅供参考 |
读这一屏的顺序建议是:先看 await 判断快不快 → 再看 aqu-sz 判断是不是在排队 → 再看 req-sz 判断负载类型 → 最后才看 %util。
%util 的定义来自机械盘时代:磁头一次只能服务一个请求,所以"有请求在处理的时间比例"就等于繁忙程度。NVMe 有几十上百个硬件队列,能同时处理大量请求,因此只要队列里一直有活,%util 就是 100%——它此时只能说明"盘没闲着",完全不能说明"盘扛不住了"。
介质的量级要刻进脑子
规划和排障时,你需要对数量级有直觉。以下是单盘的典型值(4KB 随机读):
| 介质 | IOPS 量级 | 顺序吞吐 | 典型延迟 | 每 TB 成本 |
|---|---|---|---|---|
| 7.2K SATA HDD | 100 ~ 200 | 150 ~ 250 MB/s | 5 ~ 15 ms | 最低 |
| SATA SSD | 5 万 ~ 10 万 | 500 MB/s(受限于 SATA) | 100 ~ 300 µs | 中 |
| NVMe TLC SSD | 40 万 ~ 100 万 | 3 ~ 7 GB/s | 50 ~ 150 µs | 较高 |
关键差距是随机 IOPS 差了三个数量级。这解释了很多设计决策:
- 为什么 Ceph 的元数据池、GPFS 的 metadata 一定要放 SSD——元数据操作全是小随机 I/O
- 为什么 HDD 集群做 EC 重建会那么慢——重建是大量随机读
- 为什么"混闪"方案要把 WAL/DB 放到 NVMe 上
规划时先算理论值:集群 IOPS ≈ 单盘 IOPS × 盘数 × 冗余折扣。然后按经验打 60% ~ 70% 的折——网络、CPU、软件栈、PG 分布不均都会吃掉一部分。算出来的数字用来判断"够不够",而不是用来承诺 SLA。
一块盘的 iostat 显示 r/s = 8000,rkB/s = 512000。这个负载的平均读请求大小是多少?
某 NVMe 盘 w_await 从 0.2ms 涨到 15ms,同时 aqu-sz 从 1 涨到 120,wareq-sz 保持 4KB 不变。最可能的原因是?
动手:用 elbencho 亲手验证一遍
理论看完,一定要自己跑一次。找一块空闲的测试盘(不要在生产盘上跑写测试):
# 4KB 随机读,队列深度从 1 慢慢加,观察 IOPS 与延迟怎么变
elbencho -r --direct -b 4k -t 1 --iodepth 1 --rand --randamount 10g \
-s 100g --timelimit 30 --lat --latpercent /dev/nvme0n1
# 把 --iodepth 换成 4、16、64 各跑一次,记录三组数字:
# IOPS、平均延迟、P99 延迟
把三组数字画成表,你会亲眼看到那条曲线:IOPS 先快速上升然后趋于平缓,而延迟在 IOPS 见顶之后开始陡增。那个拐点就是这块盘的可用工作区间上限。
不加 --direct,数据会走页缓存,你测的就是内存速度而不是盘速度。看到"HDD 跑出 3GB/s"这种结果,八成就是忘了这个参数。
这节课的落点
- 一次 I/O 要穿过 VFS、文件系统、页缓存、块层、驱动、设备,每层都可能放大或合并
- 记住两个式子:
吞吐 = IOPS × 块大小、队列深度 = IOPS × 延迟 - 读 iostat 的顺序:await → aqu-sz → req-sz → %util
- HDD 与 NVMe 的随机 IOPS 差三个数量级,这决定了绝大多数架构选择
延伸资料
- ·Systems Performance, 2nd Edition — Brendan Gregg
- ·k8s-in-action
k8s/etcd-disk-performance.md