"这块盘性能怎么样?""挺快的,50 万 IOPS。"——这样的对话在存储圈每天都在发生,但它几乎没有传递任何信息:是 4 KiB 还是 128 KiB?读还是写?随机还是顺序?队列深度多少?延迟是多少?p99 呢?一个不带条件的 IOPS 数字,和"这辆车很快,能跑 8000 转"一样,听着专业,其实什么也没说。
这一课把存储性能的三大指标——IOPS、吞吐(Throughput)、延迟(Latency)——以及把它们串起来的队列深度、Little 定律、利用率与饱和度讲透。学完你能:看到一个性能数字就追问出它缺失的条件;用 Little 定律心算出"这个延迟下最多能跑多少 IOPS";知道为什么平均延迟会骗人、p99 和 p99.9 到底在说什么;对从 CPU 缓存到跨洋网络的延迟数量级有直觉。本课内容主要取材于《Systems Performance, 2nd Edition》(下文简称《Systems Performance》)第 2 章与第 9 章。
三个指标,一个公式
先给出定义,用的都是块设备视角:
| 指标 | 定义 | 单位 | 典型工具里的列 |
|---|---|---|---|
| IOPS | 每秒完成的 I/O 次数 | 次/秒 | iostat 的 r/s、w/s;fio 的 IOPS= |
| 吞吐 | 每秒传输的数据量 | MB/s、MiB/s、GB/s | iostat 的 rkB/s、wkB/s;fio 的 BW= |
| 延迟 | 单个 I/O 从发起到完成的时间 | µs、ms | iostat 的 r_await;fio 的 clat、lat |
IOPS 和吞吐之间只差一个 I/O 大小:
吞吐 = IOPS × I/O 大小
4 KiB × 100,000 IOPS ≈ 400 MiB/s
128 KiB × 10,000 IOPS ≈ 1250 MiB/s
1 MiB × 3,000 IOPS ≈ 3000 MiB/s这个公式简单到不像话,但它是最常用的"防骗"工具。有人说"我们的 NVMe 跑出了 100 万 IOPS、7 GB/s",你马上可以追问:100 万 × 4 KiB = 4 GB/s,100 万 × 8 KiB = 8 GB/s——这两个数字是不是在不同的 I/O 大小下测的?大部分情况下,答案是"是":小块测 IOPS,大块测吞吐,这两个峰值不可能同时出现。
IOPS 并不等价
《Systems Performance》第 9 章专门有一节叫"IOPS Are Not Equal"。同样是 1 万 IOPS:
- 4 KiB 随机读和 1 MiB 顺序读,对设备的压力完全不同;
- 读和写不同:SSD 写要经过 FTL、可能触发垃圾回收,HDD 写可能落在盘内缓存上;
- 带
fsync/FUA 的同步写和普通写不同; - 同一块 SSD,空盘和写满后进入稳态时的写 IOPS 能差好几倍(存储硬件讲过写放大)。
所以描述一个负载,至少要给出五个属性(第 9 章 9.5.4 节的负载特征刻画):I/O 速率、吞吐、I/O 大小、读写比例、随机还是顺序。书里给的范例描述是这样的:
系统盘承受轻量随机读负载:平均 350 IOPS、3 MB/s,读占 96%,读大小约 8 KB。
偶有持续 2~5 秒的顺序写突发,峰值 4,800 IOPS、560 MB/s,写大小约 128 KB。以后写性能报告、提性能需求,就照这个格式来。
I/O 大小与随机、顺序
I/O 大小决定你撞上哪面墙
每个 I/O 都有固定开销(系统调用、块层处理、中断、设备命令处理)和按字节计的传输开销。I/O 越小,固定开销占比越大,瓶颈是 IOPS 上限;I/O 越大,瓶颈变成带宽上限(设备介质、PCIe 链路、HBA、网络)。
吞吐 (GB/s)
7 ┤ ●────●────● ← 带宽墙:PCIe 4.0 x4 / NAND 通道
6 ┤ ●
5 ┤
4 ┤ ●
3 ┤
2 ┤ ● IOPS 墙:左侧每个点的 IOPS 都接近上限
1 ┤ ●
0 ┼──●────┬────┬────┬────┬────┬────┬────
4K 8K 16K 32K 64K 128K 256K 1M I/O 大小一个实用结论:用 4 KiB 测 IOPS,用 128 KiB~1 MiB 测吞吐,中间的尺寸(16~64 KiB)往往最接近真实业务。数据库页通常是 8~16 KiB,对象存储和 AI 训练读是几百 KiB 到几 MiB。
随机与顺序
对 HDD,随机 I/O 意味着寻道 + 旋转等待。7200 转的盘转一圈 8.3 ms,平均旋转延迟约 4.2 ms,加上平均寻道 4~8 ms,一次随机 I/O 约 8~12 ms,也就是单盘 100~150 随机 IOPS;而顺序读能到 200 MB/s 以上。两者按字节算能差上百倍。
对 SSD,没有机械部件,随机和顺序读的差距小得多,但并没有消失:
- 顺序读能触发预读(Read-Ahead),块层能合并相邻请求;
- SSD 内部按页(通常 16 KiB)和擦除块组织,小的随机写会加剧写放大和垃圾回收;
- FTL 映射表缓存命中率随访问局部性变化。
队列深度与 Little 定律
队列深度是什么
**队列深度(Queue Depth,QD)**是同一时刻"在途"(已发出、未完成)的 I/O 数量。应用用同步 read() 单线程读,QD 永远是 1:发一个、等它完成、再发下一个。用 libaio/io_uring 异步提交,或开多个线程,才能让 QD 大于 1。
为什么 QD 重要?因为现代 NVMe SSD 内部有几十个 NAND 通道/Die 可以并行工作,QD=1 时只有一个在干活,其他都闲着。
QD=1: [I/O 1] [I/O 2] [I/O 3] ← 设备大部分并行单元空闲
─────────────────────────────────▶ 时间
QD=4: [I/O 1][I/O 5]
[I/O 2][I/O 6]
[I/O 3][I/O 7] ← 4 路并行,IOPS 约 ×4,延迟基本不变
[I/O 4][I/O 8]
─────────────────────────────────▶ 时间Little 定律
排队论里最有用的一条定律,适用于任何稳态系统:
L = λ × W
L:系统中的平均请求数(队列深度 / 并发度)
λ:到达率 = 稳态下的吞吐(IOPS)
W:每个请求在系统中的平均停留时间(延迟)
变形: IOPS = 并发度 / 平均延迟它不关心请求分布、服务顺序,只要系统处于稳态就成立。拿来心算极其好用:
| 场景 | 并发度 | 平均延迟 | 最大 IOPS |
|---|---|---|---|
| 单线程同步读 NVMe | 1 | 80 µs | 1 / 0.00008 = 12,500 |
| 单线程同步读 HDD | 1 | 8 ms | 125 |
| fio 4 job × iodepth 32 | 128 | 320 µs | 400,000 |
| 数据库 16 个线程同步读 Ceph RBD | 16 | 1 ms | 16,000 |
单线程 fsync 提交,每次 2 ms | 1 | 2 ms | 500 次提交/秒 |
最后一行解释了很多"数据库 TPS 上不去"的问题:瓶颈不是盘的 IOPS 上限,而是串行化的同步写延迟。这种情况下,把盘从 50 万 IOPS 换成 100 万 IOPS 毫无作用,降低单次 fsync 延迟(或做组提交)才有用。
加并发不是免费的
增加 QD 起初让 IOPS 近乎线性增长、延迟不变;当设备内部并行单元被占满,再加 QD,IOPS 不再增长,多出来的请求只能排队,延迟线性上升——这正是 Little 定律的另一面:λ 到顶后,L 增大只能让 W 增大。
IOPS 延迟
▲ ●────●────●────● ▲ ●
│ ● │ ●
│ ● │ ●
│ ● │ ●────●────●
└──┬───┬───┬───┬───┬───┬──▶ QD └──┬───┬───┬───┬───┬───┬──▶ QD
1 4 16 32 64 128 256 1 4 16 32 64 128 256
↑拐点:再加 QD 只增加延迟找到这个拐点是基准测试的核心任务之一(见基准测试):生产配置应该落在拐点左侧,给突发留出余量。
利用率与饱和度
利用率
《Systems Performance》第 2 章给出两种利用率定义:基于时间(资源忙碌的时间占比)和基于容量(已用能力占总能力的比例)。iostat 的 %util 是前者:一个采样间隔内,设备上至少有一个 I/O 在途的时间占比。
对一块同一时刻只能处理一个请求的老式 HDD,%util 接近 100% 基本等于"满了"。但对 NVMe SSD、RAID 卷、网络块设备这类能并行处理请求的设备,%util=100% 只说明"一直有活在干",并不代表"已经干不动更多活"——一个 QD=1 的负载就能把 NVMe 的 %util 顶到 100%,此时它可能只用了 5% 的能力。这是 %util 最大的坑,磁盘 I/O 观测会用实验演示。
饱和度
**饱和度(Saturation)**是超出资源处理能力、不得不排队等待的工作量。对磁盘来说,常用的饱和度指标是:
iostat的aqu-sz(平均队列长度,包含在设备上执行中的请求)持续高于设备的有效并行度;await显著高于设备本身的服务时间(排队时间在涨);- PSI 的
/proc/pressure/io中some/full持续不为零(任务因等 I/O 而停顿的时间占比)。
利用率和饱和度的关系可以这样记:利用率说明"忙不忙",饱和度说明"有没有人在排队"。100% 利用率可以没有排队,50% 利用率也可能在某些瞬间严重排队(采样间隔内前一半时间 100% 忙、后一半空闲)。
利用率与延迟的非线性
排队论告诉我们,延迟不是随利用率线性增长的。以最简单的 M/M/1 模型(随机到达、单服务台)为例,平均响应时间约为 服务时间 / (1 - 利用率):
| 利用率 | 响应时间是服务时间的 |
|---|---|
| 50% | 2 倍 |
| 70% | 3.3 倍 |
| 80% | 5 倍 |
| 90% | 10 倍 |
| 95% | 20 倍 |
真实设备不完全符合 M/M/1,但趋势一致:越接近满载,延迟涨得越快。《Systems Performance》第 9 章给出的经验是,单个磁盘利用率超过约 60% 就可能因为排队而影响性能,具体阈值取决于设备和业务的延迟要求。这也是为什么容量规划里要给存储留"水位线",而不是按 100% 能力算账(见容量与性能规划,也可以用站内的容量计算器估一估)。
延迟分布:别被平均值骗了
平均值为什么骗人
假设一个存储系统 1000 次读里,990 次命中缓存用时 0.1 ms,10 次落盘用时 50 ms:
平均延迟 = (990 × 0.1 + 10 × 50) / 1000 = 0.6 ms0.6 ms 这个平均值,没有任何一次请求真的是这个延迟。真实情况是 99% 的请求飞快、1% 的请求慢了 500 倍。如果你的 SLO 是"不能有请求超过 10 ms",平均值让你完全看不到问题。
百分位
**百分位(Percentile)**回答的是"X% 的请求比这个值快":
| 指标 | 含义 | 1 万次请求中有多少比它慢 |
|---|---|---|
| p50(中位数) | 一半请求比它快 | 5,000 |
| p90 | 90% 比它快 | 1,000 |
| p99 | 99% 比它快 | 100 |
| p99.9 | 99.9% 比它快 | 10 |
| p99.99 | 99.99% 比它快 | 1 |
| max | 最慢的那一个 | 0 |
p99 看起来是"只有 1% 的请求",但在规模面前它一点都不少。Google 的 Jeff Dean 在《The Tail at Scale》里指出:如果一次用户请求要扇出(Fan-out)到 100 个后端,每个后端只有 1% 的概率变慢,那么这次用户请求变慢的概率是:
1 - 0.99^100 ≈ 63%分布式存储天然就是扇出架构:一次大读拆成多个对象分片,一次 EC 写要等所有分片落盘,一个训练 step 要等所有 GPU 的数据都读完。在分布式系统里,单节点的尾延迟就是整个系统的常见延迟。
多峰分布
《Systems Performance》第 2 章 2.8.5 节专门讨论了多峰分布(Multimodal Distribution):存储的延迟分布经常是双峰甚至多峰的,因为请求走的路径不同。
延迟直方图(log2 分桶,典型的带缓存读路径)
usecs : count distribution
1 -> 3 : 8021 |****************************************| ← 页缓存命中
4 -> 7 : 3310 |**************** |
8 -> 15 : 402 |** |
16 -> 31 : 20 | |
32 -> 63 : 5 | |
64 -> 127 : 380 |* |
128 -> 255 : 1650 |******** | ← 落到 NVMe
256 -> 511 : 910 |**** |
512 -> 1023 : 60 | |
1024 -> 2047 : 12 | | ← 排队或 GC这种分布下,平均值落在两个峰之间的"山谷"里,恰好是最没有代表性的位置;标准差也失去意义。常见的双峰来源:
- 缓存命中 vs 未命中(页缓存、SSD 内部 DRAM、RAID 卡缓存、分布式存储客户端缓存);
- 读 vs 写(写被缓存吸收很快,或写需要复制到多副本很慢);
- 小 I/O vs 大 I/O;
- 本地副本 vs 远程副本。
结论只有一个:看延迟要看完整分布。后面的 biolatency、ext4dist 直接输出直方图,fio 输出百分位,都是为了这个。
延迟数量级
对延迟有数量级的直觉,是判断一个数字"合不合理"的前提。《Systems Performance》第 2 章表 2.2 把 3.5 GHz CPU 的一个时钟周期(约 0.3 ns)放大成 1 秒来类比,这里结合存储常见的场景整理一版(均为典型值,具体设备差异很大):
| 事件 | 典型延迟 | 放大后(1 周期 = 1 秒) |
|---|---|---|
| 1 个 CPU 周期 | 0.3 ns | 1 秒 |
| L1 缓存访问 | 0.9 ns | 3 秒 |
| L3 缓存访问 | 10 ns | 33 秒 |
| 内存(DRAM)访问 | 100 ns | 6 分钟 |
页缓存命中的 4 KiB read() | 1~5 µs | 1~5 小时 |
| RDMA 单边读(同机房) | 2~5 µs | 2~5 小时 |
| NVMe SSD 4 KiB 随机读 | 50~100 µs | 2~4 天 |
| 数据中心内 TCP 往返 | 50~500 µs | 2 天~20 天 |
| SATA SSD 4 KiB 随机读 | 100~200 µs | 4~8 天 |
| 分布式块存储(如 Ceph RBD)4 KiB 写 | 0.5~2 ms | 20 天~2 个月 |
| HDD 随机 I/O | 5~10 ms | 6 个月~1 年 |
| 北京到上海网络往返 | 约 30 ms | 3 年 |
| SCSI 命令超时 | 30 s | 3000 年 |
几个值得记住的推论:
- 页缓存命中和落到 NVMe 差 20~100 倍,落到 HDD 差 几千倍——所以 fio 忘加
direct=1测出来的数字能好看得离谱; - 分布式存储的延迟下限由网络往返和副本数决定,NVMe 本身只占一小部分,把单盘换得更快收益有限;
- 同步
fsync一次如果要 5 ms,单线程每秒最多 200 次,这是物理限制,不是配置问题。
动手练习
以下实验需要一块可以随意写坏的空闲测试盘,下文用 /dev/vdb 表示(换成你的;没有空闲盘可以用 loop 设备,但 loop 设备会经过宿主文件系统,数字只能用来观察趋势)。
验证吞吐公式。 分别用 4 KiB 和 1 MiB 做随机读,比较 IOPS 和带宽,验证
BW ≈ IOPS × bs:bashsudo apt install -y fio sysstat for bs in 4k 1m; do sudo fio --name=bs-$bs --filename=/dev/vdb --readonly --direct=1 \ --ioengine=libaio --rw=randread --bs=$bs --iodepth=16 \ --runtime=30 --time_based --group_reporting | grep -E 'IOPS=|clat \(' done验证 Little 定律。 固定
bs=4k,把--iodepth依次设为 1、4、16、64、256,记录每次的 IOPS 和平均lat,计算iodepth ÷ lat,看是否与 IOPS 吻合;画出 IOPS-QD 和延迟-QD 曲线,找到拐点。观察
%util的误导性。 在做第 2 题时另开一个终端运行iostat -xz 1,记录 QD=1 和 QD=64 时的%util、r/s、aqu-sz,思考为什么%util几乎一样而 IOPS 差很多。制造双峰分布。 在测试盘上建一个 ext4 文件系统,写入一个 2 GiB 文件,
echo 3 | sudo tee /proc/sys/vm/drop_caches后用 fio 的--direct=0 --rw=randread --size=2g读两遍,比较两次clat percentiles中 p50 和 p99 的差别(第一遍冷缓存,第二遍热缓存)。
自测
某厂商宣称其 SSD "随机读 100 万 IOPS、顺序读 7 GB/s",这两个数字能同时达到吗?为什么?
一般不能。100 万 IOPS 通常在 4 KiB 下测得,对应吞吐约 4 GB/s;7 GB/s 通常在 128 KiB 或更大块下测得,对应 IOPS 约 5 万。小块时瓶颈是每 I/O 的固定开销(IOPS 墙),大块时瓶颈是介质和链路带宽(带宽墙),两个峰值出现在不同的 I/O 大小下。
一个应用用 8 个线程做同步 4 KiB 随机读,每次读平均延迟 400 µs,它能达到的最大 IOPS 大约是多少?如果想提升 IOPS 应该怎么做?
由 Little 定律,IOPS ≈ 并发度 / 平均延迟 = 8 / 0.0004 s = 20,000。要提升 IOPS:增加并发(更多线程,或改用 io_uring/libaio 异步提交提高队列深度),或者降低单次延迟(更快的设备、减少路径上的排队、提高缓存命中率)。在设备未饱和前,增加并发几乎线性提升 IOPS。
NVMe SSD 的 %util 显示 100%,是否说明它已经满载?应该看什么来判断?
不一定。%util 是"至少有一个 I/O 在途的时间占比",NVMe 能并行处理大量请求,QD=1 的负载就可能让 %util 接近 100%,而实际只用了很小一部分能力。应该看 aqu-sz 与延迟(r_await/w_await)是否随负载上升而上升、IOPS 是否已经不再随并发增长,以及 /proc/pressure/io 等饱和度指标。
为什么说在分布式存储里,尾延迟比平均延迟更重要?
分布式存储的一次请求往往要扇出到多个节点或分片(EC 写要等所有分片、一次大读拆成多个对象、同步训练要等所有 worker),整体延迟取决于最慢的那一个。单节点 1% 的慢请求,在扇出到 100 个节点时,会让约 63% 的整体请求变慢。
延迟直方图出现两个峰(一个在 2~8 µs,一个在 128~512 µs),最可能的原因是什么?此时平均延迟有什么问题?
最可能是缓存命中与未命中:几微秒的峰是页缓存命中,几百微秒的峰是落到 NVMe SSD 的读。也可能是读写混合、不同 I/O 大小等。平均值会落在两个峰之间的"山谷",几乎没有请求真实处于这个延迟,既不能代表快路径也不能代表慢路径,应该分别分析两个峰及其占比。
参考资料
- Brendan Gregg:Systems Performance, 2nd Edition(第 2 章 2.3 概念、2.6.5 排队论、2.8 统计;第 9 章 9.3 概念)
- Jeffrey Dean, Luiz André Barroso:The Tail at Scale
- Wikipedia:Little's law
- Linux 内核文档:I/O statistics fields
- Linux 内核文档:PSI - Pressure Stall Information
- fio 官方文档
- SNIA:Solid State Storage Performance Test Specification