开始学习

Stage 02 · 进阶 · 第 1 / 7

性能指标:IOPS、吞吐与延迟

三大指标的关系、队列深度与 Little 定律、延迟分布与尾延迟。

35 分钟|全程第 11 / 38

"这块盘性能怎么样?""挺快的,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 次数次/秒iostatr/sw/s;fio 的 IOPS=
吞吐每秒传输的数据量MB/s、MiB/s、GB/siostatrkB/swkB/s;fio 的 BW=
延迟单个 I/O 从发起到完成的时间µs、msiostatr_await;fio 的 clatlat

IOPS 和吞吐之间只差一个 I/O 大小:

text
吞吐 = 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 大小、读写比例、随机还是顺序。书里给的范例描述是这样的:

text
系统盘承受轻量随机读负载:平均 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、网络)。

text
吞吐 (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 时只有一个在干活,其他都闲着。

text
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 定律

排队论里最有用的一条定律,适用于任何稳态系统:

text
L = λ × W

L:系统中的平均请求数(队列深度 / 并发度)
λ:到达率 = 稳态下的吞吐(IOPS)
W:每个请求在系统中的平均停留时间(延迟)

变形:  IOPS = 并发度 / 平均延迟

它不关心请求分布、服务顺序,只要系统处于稳态就成立。拿来心算极其好用:

场景并发度平均延迟最大 IOPS
单线程同步读 NVMe180 µs1 / 0.00008 = 12,500
单线程同步读 HDD18 ms125
fio 4 job × iodepth 32128320 µs400,000
数据库 16 个线程同步读 Ceph RBD161 ms16,000
单线程 fsync 提交,每次 2 ms12 ms500 次提交/秒

最后一行解释了很多"数据库 TPS 上不去"的问题:瓶颈不是盘的 IOPS 上限,而是串行化的同步写延迟。这种情况下,把盘从 50 万 IOPS 换成 100 万 IOPS 毫无作用,降低单次 fsync 延迟(或做组提交)才有用。

加并发不是免费的

增加 QD 起初让 IOPS 近乎线性增长、延迟不变;当设备内部并行单元被占满,再加 QD,IOPS 不再增长,多出来的请求只能排队,延迟线性上升——这正是 Little 定律的另一面:λ 到顶后,L 增大只能让 W 增大。

text
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)**是超出资源处理能力、不得不排队等待的工作量。对磁盘来说,常用的饱和度指标是:

  • iostataqu-sz(平均队列长度,包含在设备上执行中的请求)持续高于设备的有效并行度;
  • await 显著高于设备本身的服务时间(排队时间在涨);
  • PSI 的 /proc/pressure/iosome/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:

text
平均延迟 = (990 × 0.1 + 10 × 50) / 1000 = 0.6 ms

0.6 ms 这个平均值,没有任何一次请求真的是这个延迟。真实情况是 99% 的请求飞快、1% 的请求慢了 500 倍。如果你的 SLO 是"不能有请求超过 10 ms",平均值让你完全看不到问题。

百分位

**百分位(Percentile)**回答的是"X% 的请求比这个值快":

指标含义1 万次请求中有多少比它慢
p50(中位数)一半请求比它快5,000
p9090% 比它快1,000
p9999% 比它快100
p99.999.9% 比它快10
p99.9999.99% 比它快1
max最慢的那一个0

p99 看起来是"只有 1% 的请求",但在规模面前它一点都不少。Google 的 Jeff Dean 在《The Tail at Scale》里指出:如果一次用户请求要扇出(Fan-out)到 100 个后端,每个后端只有 1% 的概率变慢,那么这次用户请求变慢的概率是:

text
1 - 0.99^100 ≈ 63%

分布式存储天然就是扇出架构:一次大读拆成多个对象分片,一次 EC 写要等所有分片落盘,一个训练 step 要等所有 GPU 的数据都读完。在分布式系统里,单节点的尾延迟就是整个系统的常见延迟。

多峰分布

《Systems Performance》第 2 章 2.8.5 节专门讨论了多峰分布(Multimodal Distribution):存储的延迟分布经常是双峰甚至多峰的,因为请求走的路径不同。

text
延迟直方图(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 远程副本。

结论只有一个:看延迟要看完整分布。后面的 biolatencyext4dist 直接输出直方图,fio 输出百分位,都是为了这个。

延迟数量级

对延迟有数量级的直觉,是判断一个数字"合不合理"的前提。《Systems Performance》第 2 章表 2.2 把 3.5 GHz CPU 的一个时钟周期(约 0.3 ns)放大成 1 秒来类比,这里结合存储常见的场景整理一版(均为典型值,具体设备差异很大):

事件典型延迟放大后(1 周期 = 1 秒)
1 个 CPU 周期0.3 ns1 秒
L1 缓存访问0.9 ns3 秒
L3 缓存访问10 ns33 秒
内存(DRAM)访问100 ns6 分钟
页缓存命中的 4 KiB read()1~5 µs1~5 小时
RDMA 单边读(同机房)2~5 µs2~5 小时
NVMe SSD 4 KiB 随机读50~100 µs2~4 天
数据中心内 TCP 往返50~500 µs2 天~20 天
SATA SSD 4 KiB 随机读100~200 µs4~8 天
分布式块存储(如 Ceph RBD)4 KiB 写0.5~2 ms20 天~2 个月
HDD 随机 I/O5~10 ms6 个月~1 年
北京到上海网络往返约 30 ms3 年
SCSI 命令超时30 s3000 年

几个值得记住的推论:

  • 页缓存命中和落到 NVMe 差 20~100 倍,落到 HDD 差 几千倍——所以 fio 忘加 direct=1 测出来的数字能好看得离谱;
  • 分布式存储的延迟下限由网络往返和副本数决定,NVMe 本身只占一小部分,把单盘换得更快收益有限;
  • 同步 fsync 一次如果要 5 ms,单线程每秒最多 200 次,这是物理限制,不是配置问题。

动手练习

以下实验需要一块可以随意写坏的空闲测试盘,下文用 /dev/vdb 表示(换成你的;没有空闲盘可以用 loop 设备,但 loop 设备会经过宿主文件系统,数字只能用来观察趋势)。

  1. 验证吞吐公式。 分别用 4 KiB 和 1 MiB 做随机读,比较 IOPS 和带宽,验证 BW ≈ IOPS × bs

    bash
    sudo 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
  2. 验证 Little 定律。 固定 bs=4k,把 --iodepth 依次设为 1、4、16、64、256,记录每次的 IOPS 和平均 lat,计算 iodepth ÷ lat,看是否与 IOPS 吻合;画出 IOPS-QD 和延迟-QD 曲线,找到拐点。

  3. 观察 %util 的误导性。 在做第 2 题时另开一个终端运行 iostat -xz 1,记录 QD=1 和 QD=64 时的 %utilr/saqu-sz,思考为什么 %util 几乎一样而 IOPS 差很多。

  4. 制造双峰分布。 在测试盘上建一个 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 大小等。平均值会落在两个峰之间的"山谷",几乎没有请求真实处于这个延迟,既不能代表快路径也不能代表慢路径,应该分别分析两个峰及其占比。

参考资料

学完了吗?

进度保存在本机浏览器中,可在学习路线页查看。