存储 — 从旋转的盘片到并行队列
一句话总结
HDD 必须实际移动磁头,因此随机访问以毫秒计;SSD 没有这一限制,但擦除粒度较大,写入有其独特的成本结构。
为什么需要了解这些
数据库与文件系统的大量设计都建立在“旋转盘片”这一前提上。顺序访问比随机访问快上百倍,催生了 B 树、预写日志和日志结构合并树。存储介质变化后,必须知道哪些前提仍然成立,哪些已经失效。
工作原理
HDD。 读取一个块时,磁头先移到对应磁道(寻道通常 5~10ms),再等待目标扇区转到磁头下方(7200rpm 时平均约 4ms)。因此随机读取每秒通常只有数百次,而连续读取、不移动磁头时可达每秒数百 MB。顺序与随机相差百倍以上,这是经典存储设计的出发点。
SSD。 没有活动部件,随机读取以微秒计,但写入并不对称。NAND 闪存按页(数 KB)写入,却只能按块(数 MB)擦除;已写位置不能直接覆盖。控制器会写到新位置、把旧位置标为无效,之后搬移有效页并整块擦除(垃圾回收)。因此设备实际写入量会大于主机请求量,其比率称为写放大(write amplification)。闪存单元还有擦除次数上限,所以控制器通过磨损均衡(wear leveling)平均使用各单元。
NVMe。 SATA 只有一个深度为 32 的命令队列;NVMe 可拥有数万个队列,每个队列也可容纳数万个请求。单次请求延迟未必大幅降低,关键是增加并发请求数(队列深度)才能填满带宽。队列深度为 1 时测出的 IOPS 只能展示设备的一部分能力。
实际工作中的表现
PostgreSQL 的 random_page_cost 默认值 4.0 是按旋转磁盘设定的。在 SSD 或 NVMe 上保持该值,会让优化器把随机访问成本高估四倍,从而低估索引。不少“建了索引却不用”的问题,把它降到约 1.1 就能解决。介质改变后,基于介质的成本模型也必须调整。
仍有一些前提有效:顺序写入在 SSD 上也优于随机写入,只是原因不同。HDD 是因为不用移动磁头,SSD 则是因为能减少垃圾回收和写放大。
用数字建立直觉
用表格看存储设备的差异最清楚,数量级完全不同。
| HDD | SATA SSD | NVMe SSD | 内存 | |
|---|---|---|---|---|
| 随机读取延迟 | 5~10 ms | 0.1 ms | 0.02 ms | 0.0001 ms |
| IOPS(随机 4K) | 100~200 | 数万 | 数十万~百万 | — |
| 顺序带宽 | 150 MB/s | 500 MB/s | 3~7 GB/s | 50 GB/s+ |
| 队列深度 | 1(实际上) | 32 | 65,536 × 多个队列 | — |
HDD 随机 IOPS 只有约 200 的原因是物理运动。磁头移动和盘片旋转一次就要 5~10ms,因此数据库放在 HDD 上时,每秒约 200 次随机查询就是上限。
NVMe 比 SATA SSD 快主要源于接口而不是介质。SATA 只有一个深度 32 的队列,NVMe 则有多个深度 65,536 的队列,每个 CPU 核心都可拥有自己的队列,并行能力截然不同。
区分顺序与随机就是设计
即使是同一设备,不同访问模式的性能也可能相差十倍以上。
- 日志、备份、流处理——顺序访问,HDD 也足够,适合廉价大容量介质。
- 数据库、索引——随机访问,需要 SSD。
- 数百万个小文件——情况最差。元数据查询消耗大量 IOPS,也会耗尽文件系统 inode。
第三种情况在实际工作中很常见。若每张缩略图或每段日志都单独存文件,就可能出现磁盘容量尚有剩余但 inode 已耗尽,无法继续写入。可用 df -i 检查。
写入何时真正到达磁盘
应用调用 write() 并不表示数据已经落盘。
앱 버퍼 → 페이지 캐시(커널) → 장치 캐시 → 매체
↑ write() 는 여기까지만 보장한다
↑ fsync() 가 아래로 밀어낸다
断电时,未经过 fsync 的内容会消失。数据库每次提交都调用 fsync,这也决定了写入性能上限。
这里还有磁盘写缓存的陷阱:如果设备只写入缓存就返回“完成”,连 fsync 都会说谎。服务器级 SSD 用断电保护(PLP)电容解决此问题,这正是消费级 SSD 不适合数据库服务器的原因。
后续测验将确认什么
确认你能区分:介质变化后,哪些设计前提已经失效,哪些仍然成立。