本篇位置 承接第 23 篇。设备的物理性质决定了第 26–28 篇文件系统的样子
运行环境 容器里的 Linux。⚠️ 本篇的关键数字测不出来,理由见第四节

一、机械硬盘:一根胳膊和一张转着的盘

机械硬盘的结构,物理上就是一台唱片机

  • 盘片在转,转速固定(常见 5400 或 7200 转/分)。
  • 磁头装在一根机械臂上,能沿半径方向移动。
  • 数据存在同心圆的磁道上,磁道再分成扇区

要读一个扇区,得干三件事:

  1. 寻道(seek):把机械臂移到那条磁道上。几毫秒。
  2. 旋转等待(rotational latency):等那个扇区转到磁头底下。平均是转半圈的时间。
  3. 传输:读数据。这一步很快。

⭐ 前两步是机械运动,这就是全部麻烦的来源。

二、⭐ 算一笔可以验算的账

这笔账不需要任何测量,纯算术,你可以自己核:

7200 转/分的盘

  • 转一圈:60 秒 ÷ 7200 = 8.33 毫秒
  • 平均旋转等待 = 转半圈 = 4.17 毫秒
  • 平均寻道:厂商标称通常 4 到 9 毫秒,取 4.5

读一个随机的 4 KB 块:

4.5(寻道)+ 4.17(旋转)+ 约 0.03(传输)≈ 8.7 毫秒
→ 每秒约 115 次   (115 IOPS)

顺序读 4 KB(磁头已经在正确位置,接着往下读):

持续传输率约 150 MB/s → 4 KB 只要 0.027 毫秒
→ 每秒约 37000 次

相差约 320 倍。

这 320 倍就是此后三十年文件系统设计的全部动机。 第 26 到 28 篇里每一个"看起来很绕"的设计——把 inode 和数据放近、按块组分配、日志追加写——动机都能追溯到这个数字

⚠️ 顺带一提:厂商标的"7200 转"和"150 MB/s"是可以查证的公开参数,上面全是算术。下一节要说的是,为什么我不能在这台机器上把它测给你看。

三、⚠️ 这个环境测不出这件事

我试了。用 O_DIRECT(绕过页缓存)读一个 512 MB 的文件,8000 次 4 KB 读:

  顺序读      8000 次 4096 字节直读   用时  0.02 秒     455686 IOPS   每次    2.2 µs
  随机读      8000 次 4096 字节直读   用时  0.02 秒     374705 IOPS   每次    2.7 µs

45 万 IOPS,每次 2.2 微秒。 这不是磁盘的速度,这是内存的速度

原因有两层:

  1. 容器的文件系统是 overlayfs,它对 O_DIRECT 的支持是有限的——请求可能根本没有绕过缓存。
  2. 更根本的是:这台"机器"是一台虚拟机,它的"磁盘"是宿主 macOS 上的一个文件。就算穿透了客户机的缓存,还有宿主机的缓存在下面接着。

所以这两个数字是真实的,但它们量的不是我想量的东西。 按这门课第 1 篇立的规矩,我不拿它论证任何关于磁盘的结论。

要真的看到那 320 倍,你需要一块物理的机械硬盘,和 fio 这样的工具。 在云主机和容器里,你测到的永远是虚拟化层的性质。

⚠️ 这件事本身值得记住,因为它是性能测试里最常见的错误你以为你在测存储,其实你在测缓存。

四、SSD:拿掉了胳膊,换来一条怪规则

固态盘没有机械运动,随机和顺序的差距一下子小了两三个数量级。

但它有一条非常别扭的物理限制:

读,可以按页(通常 4–16 KB)。 写,也可以按页——但只能写"已经擦干净"的页。 擦除,只能按块(通常 128–256 页,几 MB)。

“能按页写,但只能按块擦” ——这一条不对称,是 SSD 所有复杂性的源头。

想改一个 4 KB 的页怎么办?不能就地改(那一页不干净),你得:

  1. 把整个块(几 MB)读出来,
  2. 擦掉整块,
  3. 把改过的内容连同其它没动的页一起写回去。

改 4 KB,实际擦写了几 MB。

而且闪存单元有寿命——每个块能擦写的次数是有限的(几百到几万次,取决于工艺)。如果某个块被反复擦写,它很快就坏了。

FTL:又一层中间人

SSD 内部有一个闪存转换层(FTL),它干的事和第 12 篇的页表惊人地像

虚拟内存 SSD 的 FTL
上层看到的 虚拟地址 逻辑块地址(LBA)
实际的 物理帧 闪存的物理页
中间那张表 页表 映射表
谁维护 操作系统 SSD 里的控制器

有了这层,改一个 4 KB 页就不用擦整块了:把新数据写到一个别处的干净页上,然后改一下映射表,让那个逻辑地址指向新位置。 旧的那页标成"无效"。

“不原地改,写到新地方再改指针”——这一招你会在第 28 篇的写时复制文件系统里一字不差地再见到一次。

代价是旧页越积越多,于是需要垃圾回收:挑一个无效页很多的块,把还有效的页搬到别处,然后整块擦掉。

写放大

垃圾回收要搬数据,搬数据也是写。于是:

写放大 = 闪存实际写入的字节数 ÷ 你请求写入的字节数

你写 1 GB,SSD 内部可能写了 3 GB。寿命按内部的算,不按你请求的算。

影响写放大的因素很实在:

  • 预留空间(over-provisioning):盘里留一部分你看不见的空间给垃圾回收周转。留得多,写放大低。 企业级 SSD 预留得比消费级多得多,这是它们贵的原因之一。
  • 盘有多满:越满,垃圾回收越难找到"几乎全是无效页"的块,搬的数据越多。⚠️ SSD 快写满的时候会明显变慢,这就是原因。
  • 你的写入模式:⭐ 顺序覆盖写的写放大接近 1,小块随机写可以到几十。

所以"SSD 上顺序和随机没区别"是不对的。 读确实差不多;写差得远

TRIM

还有一个跨层的问题:你删了一个文件,SSD 不知道。

从 SSD 的角度,那些逻辑块只是"暂时没人写",它必须继续保留内容——万一你要读呢。于是垃圾回收还在辛辛苦苦搬运一堆早就没用的数据。

TRIM 命令就是文件系统主动告诉 SSD:“这些块我不要了,随便扔。”

⭐ 这是一个很好的例子:分层带来的信息丢失,必须靠一个专门的接口把信息补回去。 你会在第 30 篇的虚拟化里再看到同类的问题(内存气球、大页共享)。

五、这对上层意味着什么

机械硬盘 SSD
随机读 vs 顺序读 差几百倍 差几倍以内
随机写 vs 顺序写 差几百倍 依然差很多(写放大)
寿命 机械磨损 擦写次数有限
该优化什么 减少寻道:数据放近、顺序访问 减少小块随机写:批量、追加

⭐ 有意思的是,两种设备给出的建议方向是一致的:尽量顺序、尽量批量。 只是原因完全不同——一个是为了不动机械臂,一个是为了不触发垃圾回收。

这就是为什么为机械硬盘设计的"日志结构"思想,在 SSD 时代不但没过时,反而更重要了(第 28 篇,以及所有 LSM-tree 存储引擎)。

⚠️ 但有一条建议是过时的:“把相关数据放在相邻的块上"对 SSD 没意义——FTL 早就把你的逻辑地址打散到别处去了。你看到的"相邻"是假的。 这是第 1 篇那个"谎言"主题在存储层的又一次现身。

六、代价与取舍

SSD 换来了什么: 随机读快了三个数量级,功耗和抗震性也好得多。

它花了什么:

  • 一层你控制不了的中间人。 FTL 在盘里,是厂商的固件,行为不透明。你的性能取决于一段你看不到的代码。
  • 寿命有限,而且实际消耗的寿命取决于写放大,也就是取决于你的写入模式。
  • 延迟长尾。 垃圾回收随时可能触发,一次本该 100 微秒的写偶尔会花几毫秒。对 p99 延迟敏感的系统这是个真问题。

它放弃了什么: “逻辑地址反映物理位置"这个假设。 上层为机械硬盘做的很多布局优化,在 SSD 上变成了无效的猜测。

七、小结

  • 机械硬盘读一个扇区要寻道 + 旋转等待 + 传输,前两步是机械运动。
  • 可验算的算术:7200 转 → 平均旋转等待 4.17 ms;加上寻道约 4.5 ms → 随机 4 KB 读约 115 IOPS,顺序约 37000 IOPS。相差约 320 倍——这是三十年文件系统设计的全部动机。
  • ⚠️ 这个数字在容器/虚拟机里测不出来:实测顺序 2.2 µs、随机 2.7 µs,那是内存和虚拟化层的速度。你以为在测存储,其实在测缓存——这是性能测试最常见的错误。
  • SSD 的怪规则:⭐ 能按页写,但只能按块擦。 于是有了 FTL——⭐ “不原地改,写到新地方再改指针”,和页表是同一个结构,和第 28 篇的写时复制是同一招。
  • 写放大 = 实际写入 ÷ 请求写入。受预留空间、盘的满度、写入模式影响。⭐ 顺序覆盖写接近 1,小块随机写可以到几十——所以"SSD 随机和顺序没区别"只对读成立。
  • TRIM 是把"文件被删了"这个信息补给 SSD 的专门接口。⭐ 分层丢失的信息,得靠专门的接口补回去。
  • 两种设备的建议方向一致:顺序、批量。⚠️ 但"把相关数据放在相邻块上"对 SSD 无效——FTL 已经把它打散了,你看到的"相邻"是假的

思考题

  1. 第二节算出随机读约 115 IOPS。如果换成 15000 转的企业盘、寻道 3 毫秒,是多少?这个提升值不值它的价钱?
  2. 一块 SSD 标称"总写入量 600 TB”。如果你的负载写放大是 4,你实际能写多少数据?怎么设计写入模式把这个数字提上去?
  3. 第四节说盘越满写放大越高。有一个非常简单的用户侧办法能缓解它——是什么?为什么很多人不知道该这么做?
  4. FTL 和页表结构一样。那么,FTL 有没有对应的"TLB”?它的映射表存在哪里?断电了怎么办?

延伸