| 本篇位置 | 承接第 23 篇。设备的物理性质决定了第 26–28 篇文件系统的样子 |
| 运行环境 | 容器里的 Linux。⚠️ 本篇的关键数字测不出来,理由见第四节 |
一、机械硬盘:一根胳膊和一张转着的盘
机械硬盘的结构,物理上就是一台唱片机:
- 盘片在转,转速固定(常见 5400 或 7200 转/分)。
- 磁头装在一根机械臂上,能沿半径方向移动。
- 数据存在同心圆的磁道上,磁道再分成扇区。
要读一个扇区,得干三件事:
- 寻道(seek):把机械臂移到那条磁道上。几毫秒。
- 旋转等待(rotational latency):等那个扇区转到磁头底下。平均是转半圈的时间。
- 传输:读数据。这一步很快。
⭐ 前两步是机械运动,这就是全部麻烦的来源。
二、⭐ 算一笔可以验算的账
这笔账不需要任何测量,纯算术,你可以自己核:
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 微秒。 这不是磁盘的速度,这是内存的速度。
原因有两层:
- 容器的文件系统是 overlayfs,它对
O_DIRECT的支持是有限的——请求可能根本没有绕过缓存。 - 更根本的是:这台"机器"是一台虚拟机,它的"磁盘"是宿主 macOS 上的一个文件。就算穿透了客户机的缓存,还有宿主机的缓存在下面接着。
⭐ 所以这两个数字是真实的,但它们量的不是我想量的东西。 按这门课第 1 篇立的规矩,我不拿它论证任何关于磁盘的结论。
要真的看到那 320 倍,你需要一块物理的机械硬盘,和 fio 这样的工具。 在云主机和容器里,你测到的永远是虚拟化层的性质。
⚠️ 这件事本身值得记住,因为它是性能测试里最常见的错误:你以为你在测存储,其实你在测缓存。
四、SSD:拿掉了胳膊,换来一条怪规则
固态盘没有机械运动,随机和顺序的差距一下子小了两三个数量级。
但它有一条非常别扭的物理限制:
读,可以按页(通常 4–16 KB)。 写,也可以按页——但只能写"已经擦干净"的页。 擦除,只能按块(通常 128–256 页,几 MB)。
⭐ “能按页写,但只能按块擦” ——这一条不对称,是 SSD 所有复杂性的源头。
想改一个 4 KB 的页怎么办?不能就地改(那一页不干净),你得:
- 把整个块(几 MB)读出来,
- 擦掉整块,
- 把改过的内容连同其它没动的页一起写回去。
改 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 已经把它打散了,你看到的"相邻"是假的。
思考题
- 第二节算出随机读约 115 IOPS。如果换成 15000 转的企业盘、寻道 3 毫秒,是多少?这个提升值不值它的价钱?
- 一块 SSD 标称"总写入量 600 TB”。如果你的负载写放大是 4,你实际能写多少数据?怎么设计写入模式把这个数字提上去?
- 第四节说盘越满写放大越高。有一个非常简单的用户侧办法能缓解它——是什么?为什么很多人不知道该这么做?
- FTL 和页表结构一样。那么,FTL 有没有对应的"TLB”?它的映射表存在哪里?断电了怎么办?