本文是 机械硬盘工作原理 的进阶篇。与基础篇面向「了解概念」不同,本文站在数据库内核开发工程师的立场:你要写的不是”如何读写一个文件”,而是缓冲池、WAL、B-Tree、checkpoint、崩溃恢复——这些模块的正确性都建立在对磁盘行为的精确假设之上。凡是假设错了就会出数据损坏或丢事务的地方,本文重点展开。
零、为什么数据库内核必须懂 HDD
数据库内核的几乎每一个性能与正确性决策,最终都要落到「这一次磁盘 IO 到底发生了什么」:
- 正确性:
fsync()之后事务是否真的不会丢?断电瞬间正在写的 8KB 页面会不会半截落盘(torn page)? - 性能:为什么 B-Tree 的随机读比顺序扫描慢几百倍?为什么 WAL 必须是顺序写?
- 可靠性:消费级”叠瓦盘”做数据库盘为什么会周期性卡死?RAID 重建时硬盘为何被踢出?
把这些问题答错,上层再精巧的算法都是空中楼阁。下面逐层拆解。
一、物理构成:随机访问为什么贵
一块 HDD 的核心部件:
┌─────────────────────────────────────────────┐
│ 主轴电机 (Spindle) 恒速旋转 7200/10k/15k RPM │
│ ┌───────────────────────────┐ │
│ │ 盘片 Platter ×N (双面涂磁) │ ← 同心磁道 │
│ └───────────────────────────┘ │
│ ┌───────────────────────────┐ │
│ │ actuator arm 执臂 │ │
│ │ └ 磁头 Head (GMR 传感器) │ ← 半径摆动 │
│ │ 音圈马达 VCM 驱动 │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────────┘
- 磁道 (Track):盘片上同心圆;柱面 (Cylinder):各盘片同一半径的磁道集合(切换磁头比移动臂快)。
- 扇区 (Sector):磁道被切分的弧段,是最小读写单元。
- 磁头是巨磁阻 (GMR) 传感器,非接触式悬浮在盘片上方约几纳米,靠感应介质剩磁方向读出模拟电压。
随机访问昂贵,根本原因是机械运动:移动磁头(寻道)+ 转动盘片(旋转等待)都要毫秒级,而数据传输本身不到 0.1ms。这是后面所有优化(顺序化、预读、缓冲池)的出发点。
二、寻址模型:LBA、CHS 与扇区尺寸(对齐是 DB 的地基)
2.1 LBA → 物理位置的翻译
操作系统和数据库只看到连续编号的 逻辑块地址 (LBA)。控制器把它翻译成物理的 柱面/磁头/扇区 (CHS) 再驱动硬件:
逻辑世界 物理世界
┌──────────┐ ┌──────────────────────┐
│ LBA 2048 │ 固件翻译 → │ 柱面 k / 磁头 h / 扇区 s │
└──────────┘ └──────────────────────┘
现代硬盘用 区位记录 (Zone Bit Recording):外圈磁道更长、扇区更多,所以「LBA→物理位置」由固件内部的区映射表完成。数据库要知道的关键事实是:相邻 LBA 在物理上基本相邻——这正是顺序 IO 快、随机 IO 慢的根本原因。
2.2 扇区尺寸:512n / 512e / 4Kn(DB 内核必懂)
这是最容易被忽视、却最影响正确性与性能的底层细节。
| 类型 | 逻辑扇区 | 物理扇区 | 说明 |
|---|---|---|---|
| 512n | 512B | 512B | 传统盘,无翻译开销 |
| 512e | 512B | 4KiB | 仿真模式,最普遍 |
| 4Kn | 4KiB | 4KiB | 原生 4K,需 OS/驱动支持 |
- 512e 的 RMW 放大:物理扇区是 4KiB,但对外暴露 512B 逻辑块。当你写一个未对齐的 4KiB(跨两个物理扇区)或写小于 4KiB 的块时,盘必须先把整个 4KiB 物理扇区读出 → 修改 → 整体写回(read-modify-write),一次逻辑写变成一次读 + 一次写,延迟与写放大翻倍。
- 对齐:文件系统/分区/LVM 必须 4KiB 对齐,数据库页(通常 8KiB)也要按 4KiB 边界起址,否则每个页面写都可能触发 RMW。
- 原子写单元:盘保证「一个物理扇区 (4KiB) 要么完整写入、要么完全没写」。但数据库页面常是 8KiB,跨两个 4KiB 物理扇区。若断电正好发生在写第一个扇区之后、第二个之前——就会出现 torn page(撕裂页):页面一半是新数据一半是旧数据,逻辑上完全不一致。这正是 WAL 里 full-page write(整页写前像) 存在的根本原因——崩溃恢复时用 WAL 里记录的整页镜像覆盖回这个损坏页。
内核开发要诀:永远假设一次”页面写”不是原子的。用 page checksum 检测撕裂/静默损坏,用 WAL full-page 在恢复时重建;并要求存储层 4KiB 对齐,避免 512e 的 RMW。
三、一次随机读:端到端全链路
下图是一次随机读取从发令到返回的完整路径(以 7200 RPM 为例):
flowchart TD
subgraph H0["① 主机 ↔ 控制器"]
A["主机: READ LBA n, 长度"]
B["控制器: LBA → CHS(柱面/磁头/扇区)"]
end
subgraph M0["② 机械运动 — 随机读的主要代价 (毫秒级)"]
C["音圈马达寻道: 磁头移到目标磁道<br/>Seek 3–15 ms"]
D["伺服定位: 锁定磁道中心 Track Following"]
E["旋转等待: 目标扇区转到磁头下<br/>Rotational Latency ≈ 4.2 ms @7200RPM"]
end
subgraph S0["③ 读通道信号处理 (微秒级)"]
F["磁头感应磁通变化 → 模拟电压"]
G["预放 + PRML 读通道: 模拟转数字比特"]
H["ECC 解码 + 校验"]
end
subgraph R0["④ 回传 (传输 < 0.1 ms)"]
I["写入驱动缓存, 经 SATA/SAS 回主机"]
J["数据返回"]
end
A --> B --> C --> D --> E --> F --> G --> H --> I --> J
classDef host fill:#e7f5ff,stroke:#1c7ed6,color:#103a5c;
classDef mech fill:#fde2e1,stroke:#e03131,color:#7a1416;
classDef sig fill:#e6fcf5,stroke:#0ca678,color:#0a4f3c;
classDef back fill:#fff3bf,stroke:#f08c00,color:#7a4b00;
class A,B host;
class C,D,E mech;
class F,G,H sig;
class I,J back;
下图用时序图展示”发令 → 控制器翻译 → 盘片感应 → 返回”的协作过程:
sequenceDiagram
participant H as "主机 Host"
participant C as "控制器 Controller"
participant P as "盘片 Platter"
H->>C: READ LBA n, 长度
C->>C: LBA → 柱面/磁头/扇区
C->>P: 寻道 + 旋转等待
P-->>C: 磁通变化 → 模拟电压
C->>C: PRML 译码 + ECC 校验
C-->>H: 数据经 SATA/SAS 返回
3.1 寻道 (Seek) —— 最贵的一步
磁头装在 音圈马达 (VCM) 驱动的臂上,通电解洛伦兹力沿半径摆动,是典型闭环控制:
目标位置 ──┐
│ 位置误差信号 PES
伺服逻辑 ─┴─→ 驱动 VCM ─→ 磁头移动
▲ │
└── 从伺服扇区读回实际位置 (闭环校正)
- 全程寻道 (full stroke) 约 8-15ms,平均寻道约为全程的 1/3。
- 寻道功耗大、有机械磨损,是随机访问的主要成本。
3.2 伺服定位与磁道跟踪
盘片嵌入 伺服扇区 (Servo Sector),周期性记录精确位置刻度。磁头读伺服得到两路 burst 信号,比较幅度算出 位置误差信号 (PES),微调 VCM 让磁头始终居中:
[数据][伺服][数据][伺服][数据][伺服]... ← 每磁道间插入伺服
PES > 0 : 偏右 → 向左微调 PES < 0 : 偏左 → 向右微调
没有伺服,磁头会因振动/热膨胀飘出磁道——硬盘怕震动的根源。
3.3 旋转等待 (Rotational Latency)
7200 RPM → 每秒 120 圈 → 每圈 8.33ms → 平均旋转等待 ≈ 4.17ms
15000 RPM → 每圈 4ms → 平均旋转等待 ≈ 2ms
3.4 读出:从磁场到比特(PRML)
磁头输出微弱模拟电压,经预放、可变增益、低通、ADC 采样后送入 PRML(局部响应最大似然) 通道:
磁通翻转 → 磁头感应电压 → 预放 → VGA+低通 → ADC → PRML 均衡
→ Viterbi 最大似然译码 → 比特流 → ECC 纠错
记录密度高时相邻比特脉冲会码间串扰叠加。PRML 不逐点判 0/1,而是把整段采样序列送入 Viterbi 译码器,在已知信道模型下找最可能产生该波形的比特序列——这是容量持续翻倍的关键技术。现代盘 ECC 用 LDPC 码,可纠多位错误。
3.5 扇区内部结构
┌────────┬────────┬──────────┬──────────┬─────┐
│ Preamble│ Sync │ 用户数据 │ ECC/CRC │ Gap │
│ 前导同步│ 标记 │ (512B) │ 校验码 │间隔 │
└────────┴────────┴──────────┴──────────┴─────┘
ECC 失败,控制器重试;多次失败报 介质错误 (UNC)——数据库偶发的只读失败来源。
四、一次写入:写路径全景与 fsync 语义(DB 内核最核心)
写入前几步(寻道、旋转等待)与读取一致,差异在「把数据放上介质」:
flowchart TD
A["主机: WRITE LBA n, 数据"] --> B["控制器: LBA 转物理位置"]
B --> C["寻道 + 旋转等待"]
C --> D["写电流磁化介质: 线圈磁场翻转剩磁"]
D --> E["按编码写比特: 磁通翻转方向表示 0/1"]
E --> F["追加 ECC + 前导/同步"]
F --> G["回读校验 Read-After-Write"]
G --> H["报告完成, 或重写/重映射坏块"]
下图用时序图对比”普通 write()“与”fsync 强制落盘”的差异:
sequenceDiagram
participant App as "应用"
participant PC as "页缓存(易失)"
participant BL as "块层"
participant DC as "盘缓存(易失)"
participant PL as "盘片(非易失)"
App->>PC: write() 拷贝即返回
Note over PC,DC: 回写线程异步刷出
PC->>BL: WRITE
BL->>DC: 进缓存即 ACK
App->>PC: fsync()
PC->>BL: 强制刷出脏页
BL->>DC: FLUSH CACHE / FUA
DC->>PL: 真正落盘
PL-->>App: durable commit
但 DB 内核真正要搞懂的,是从 write() 系统调用到数据真正掉不丢,中间经过了哪几层缓存——这才是持久化语义的全部真相。
4.1 一条写经历的完整层级
应用进程
│ write() ← 只把数据拷进内核页缓存, 立即返回!
▼
内核页缓存 (Page Cache, 易失, 在主机内存)
│ 由 pdflush/回写线程 异步刷出 (脏页超时/超阈值)
▼
块层 (Block Layer / blk-mq) ← IO 调度、合并、重排
│ 下发 SCSI/SATA 命令 (WRITE)
▼
硬盘 DRAM 写缓存 (易失!) ← 数据进缓存即回 ACK, 随后异步落盘
│ 由盘固件异步刷到盘片
▼
盘片介质 (非易失, 真正持久)
关键点:write() 返回 ≠ 数据落盘。它只进了主机内存的页缓存。真正持久要经过:页缓存被回写线程刷出 → 块层 → 盘内 DRAM 缓存(依然易失)→ 盘片。中间任何一层掉电都可能丢数据。
4.2 fsync 到底做了什么
fsync(fd) 要保证持久,必须击穿两层易失缓存:
- 刷主机页缓存:把该文件相关的脏页强制写回块层(绕过”异步回写”)。
- 刷盘内 DRAM 缓存:在命令里加 FLUSH CACHE(或对该次写用 FUA — Force Unit Access,绕过盘缓存直接落盘后才 ACK)。
只有二者都发生,事务提交才算 durable。如果盘缓存是易失的、且 fsync 没发 FLUSH/FUA,那么”已提交事务”其实只在盘的内存里,断电即丢——WAL 也救不回来,因为 WAL 记录本身也没真落盘。
4.3 写屏障与顺序保证
光”落盘”还不够,还要保证顺序:WAL 的提交记录必须先于数据页落盘,否则崩溃时可能看到”数据改了但 WAL 没记”。早期内核用 write barrier 强制这种顺序;现代 blk-mq 下,日志文件系统/数据库用「flush + FUA」显式冲刷来替代屏障。数据库内核要清楚:你的 fsync 顺序依赖,最终是靠底层 flush 命令的保序实现的。
4.4 非易失缓存(PLP)——“ACK 即持久”成立的条件
它是什么
PLP(Power-Loss Protection,掉电保护)是企业级 HDD / RAID 卡 / 企业 SSD 上的一项硬件兜底电路:在电路板上集成电容(或超级电容 / 电池),并配合专门固件逻辑。电容平时处于充电待命状态,它攒下的电能只用于一件事——断电后的”最后一刷”。
其工作时序如下:外部供电一旦消失,盘立即检测到掉电 → 切换由电容储备电能供电 → 固件在电容维持的几十毫秒窗口内,把 DRAM 写缓存里尚未落盘的数据强制刷到非易失介质(HDD 刷到盘片,SSD 刷到 NAND)→ 数据落定后供电才彻底消失。
sequenceDiagram
participant P as "外部供电"
participant C as "电容(PLP)"
participant D as "DRAM 写缓存(易失)"
participant M as "非易失介质(盘片/NAND)"
P->>C: 正常时持续给电容充电待命
P--xP: 供电中断!
C->>D: 电容接管供电(几十 ms 窗口)
D->>M: 固件强制把缓存数据刷出
M-->>C: 数据落定, 可安全掉电
它解决什么问题
普通消费级 HDD 的 DRAM 写缓存是易失的:开启 write-back 模式后,一次写只要进缓存就向主机回 ACK,数据随后才异步落盘。此时 fsync() 只保证数据”进了盘缓存”,并没进盘片。一旦这期间断电,缓存里的数据全部蒸发——事务已经向客户端报告”提交成功”,底层却实际丢失,直接违反 ACID 的 D(持久性)。
因此没有 PLP 的盘,生产库要么关掉易失写缓存(write-through,每次写直落盘片、性能骤降),要么每次写都走 FUA / 写穿透。而有了 PLP,write-back 缓存就变得”安全”:断电时电容兜底把缓存刷盘,于是数据库可以一边开着写缓存享受高吞吐,一边保证已 ACK 的数据不丢。这正是企业盘能撑住 OLTP IOPS 的关键前提。
内核开发要诀:durable commit 的充要条件 = 关键写经 FUA/写穿透或 PLP 非易失缓存真正落盘 + flush 保序 + 上层
fsync()真强制刷。WAL 提供的是崩溃重放能力,而非替代底层持久化——别把 WAL 当掉电保险丝。
4.5 实操:如何检测一块盘是否带 PLP
PLP 没有统一的标准探测位,但生产环境可用下面几条命令快速判断一块盘”到底有没有掉电保护”:
# 1) 内核视角:盘写缓存当前是 write_back(开了) 还是 write_through(写穿透)
cat /sys/block/sda/queue/write_cache
# 2) 多数企业盘会在 SMART 信息里自报 PLP 状态 (Seagate Exos / WD Gold / 等)
smartctl -a /dev/sda | grep -i "power loss"
# 命中示例: Power Loss Protection: Enabled
# 3) SAS/SCSI 盘查看写缓存是否开启 (WCE 位)
sdparm --get=WCE /dev/sda
# 4) NVMe 盘:看 Volatile Write Cache 位 (vwc);PLP 一般由厂商工具/SMART 报告
nvme id-ctrl /dev/nvme0 | grep -i vwc
解读要点:
write_cache = write_back且确认有 PLP → 可放心开缓存,兼顾高性能与”ACK 即持久”。write_cache = write_back但查不到 PLP(消费级盘常态)→ 视为易失,生产库必须走 FUA/写穿透,或显式关缓存:hdparm -W0 /dev/sda # SATA 消费盘:关闭写缓存 sdparm --set=WCE=0 /dev/sda # SAS/SCSI 盘:关闭写缓存write_cache = write_through→ 已写穿透,每次写直达介质,安全但慢。
经验法则:拿不准就当没有 PLP。在无法 100% 确认带电容备份的盘上跑数据库,宁可牺牲一点写性能把易失写缓存关掉,也不要让”已提交事务”暴露在掉电丢失风险里。
五、SMR 叠瓦式记录:数据库工程师的隐形地雷
很多”数据库盘莫名周期性卡死”的案子,元凶是 SMR(叠瓦式磁记录)。
5.1 原理
为提升密度,SMR 让相邻磁道像屋顶瓦片一样部分重叠:写一条磁道会破坏下游磁道的数据,因此必须连带着把下游所有磁道一起重写。
普通 PMR: ┌──┐┌──┐┌──┐┌──┐ 磁道互不重叠, 可随机写
track0 track1 track2 track3
SMR 叠瓦: ┌────┐
┌────┐│
┌────┐││ 写 track1 必须重写 track2, track3... (RMW 风暴)
盘被分成若干 zone(区),区内只能顺序写,改写任意一处都要 RMW 整个下游——延迟从毫秒级跳到几百毫秒甚至秒级。
5.2 两类 SMR 对数据库的影响
| 类型 | 管理方式 | 数据库风险 |
|---|---|---|
| DM-SMR(设备托管) | 固件偷偷做 RMW,对主机透明 | 最危险:随机写时固件静默触发 RMW,尾延迟爆炸且不可预测 |
| HA-SMR(主机感知) | 需 FS/应用按 zone 顺序写、显式 reset | 若数据库不感知 zone,写满即错;需专门适配 |
WAL 是顺序追加,理论上 SMR 还能忍;但 checkpoint、后台刷脏、Compaction、VACUUM 这些”改写随机位置”的操作会疯狂触发 RMW,造成周期性写入卡顿与极高尾延迟。
5.3 识别与规避
- 用
smartctl -i看型号,查厂商是否标注 SMR(常见大容量冷存储盘、部分桌面盘)。 - 生产数据库坚决不用消费级 SMR 盘;选 CMR/PMR 企业或近线盘。
六、NCQ、队列深度与 IO 调度(吞吐 vs 延迟的博弈)
6.1 NCQ 重排
NCQ (Native Command Queuing) 允许主机一次下发最多 32 个 IO,固件重排让磁头沿最短路径单向服务,减少来回摆动:
主机下发: 读磁道 100 → 50 → 200 → 30
NCQ 重排: 30 → 50 → 100 → 200 ← 磁头单向扫过
6.2 队列深度:不是越深越好
- 过浅:磁头在一次 IO 间空隙无法被其他 IO 填补,吞吐上不去。
- 过深:大量 IO 在盘内排队,每个 IO 的延迟被拉长(吞吐升、延迟炸)。数据库 OLTP 要低延迟,盲目加大队列深度会牺牲尾延迟。
6.3 Linux 块层调度器(blk-mq)
none : 不做调度, 直接提交 (适合无机械seek的 NVMe; HDD 也可配但非最优)
mq-deadline : 保证请求 deadline, HDD 随机/混合负载的稳妥默认
kyber : 基于延迟目标动态限流, 适合混合负载
bfq | 桌面/公平调度, 一般不用于数据库服务器
DB 内核视角:HDD 上常用 mq-deadline(兼顾吞吐与延迟上界);纯大块顺序 IO 可配 none;NVMe 默认 none。调度器影响”随机 IO 是否被合并/排序”,直接关系到寻道次数。
七、延迟、IOPS、吞吐与尾延迟
7.1 拆解公式
随机读延迟 ≈ 寻道(8ms) + 旋转(4.2ms) + 传输(<0.1ms) + 控制器/协议开销
≈ 12.25ms
随机读 IOPS ≈ 1 / 0.01225 ≈ 80 IOPS
对比 SSD 随机读 数万~数十万 IOPS,差 3 个数量级。
7.2 平均值会骗人:尾延迟与排队效应
数据库最怕的不是平均 12ms,而是 p99.9 可能到几百毫秒。原因在于排队:当 IO 到达率 λ 接近服务率 μ,等待时间按 M/M/1 直觉 W = 1/(μ−λ) 非线性飙升。HDD 的 μ 很低(约 80 IOPS),稍有并发队列就堵。这就是 HDD 不适合低延迟 OLTP 的根本原因——不是单次慢,而是一忙就雪崩。
八、错误处理与数据完整性
- UNC(不可纠正错误):扇区 ECC 失败,读请求返回错误。数据库层表现为偶发读失败——要靠 page checksum 在内存/落盘时检测,副本 / RAID 兜底。
- ERC / TLER(错误恢复时限):企业盘固件把错误恢复限制在较短时间(如 7s)内,超时即把错误上交主机;消费盘可能重试几十秒。在 RAID 场景下,消费盘长时间”无响应”会被 RAID 卡误判为掉盘、触发重建风暴——数据库服务器要用带 ERC 的企业盘。
- 端到端保护(DIX/DIF):在 SCSI 层对数据加保护信息,防止”盘内正确但传输中损坏”,企业存储可选。
九、设备级读写放大(DB 写入成本的隐藏项)
| 来源 | 放大机制 | DB 影响 |
|---|---|---|
| 512e 不对齐 | 亚 4KiB / 跨边界写触发 RMW | 写延迟翻倍 |
| SMR 叠瓦 | 随机写触发下游整区 RMW | 尾延迟爆炸 |
| 小块写凑整 | 写小于扇区被凑整到扇区 | 写放大 |
| WAL + 数据双写 | 同一修改写 WAL 再写数据页 | 逻辑放大(必要代价) |
十、对数据库内核的综合启示
| HDD 特性 | 数据库内核应对 |
|---|---|
| 寻道/旋转占主导,随机极慢 | 缓冲池把随机读转内存命中;B-Tree 索引减少磁盘访问次数 |
| 相邻 LBA 物理相邻 | 聚簇/堆组织表、按主键顺序写减少磁头摆动 |
| 顺序写快 | WAL 顺序追加;批量导入 COPY;避免随机小写 |
| 512e 不对齐 RMW | 全栈 4KiB 对齐;页面尺寸按 4KiB 边界起址 |
| 写非原子 / torn page | WAL full-page write + page checksum 检测与恢复 |
| 盘缓存掉电易失 | FUA/写穿透 或 PLP 非易失缓存 + fsync() 真强制落盘 + flush 保序 |
| SMR 随机写 RMW | 生产禁用消费级 SMR,选用 CMR 企业盘 |
| NCQ 重排 | 适度并发 IO 提升吞吐;控制队列深度保低延迟 |
| 低 μ + 排队雪崩 | HDD 只承载大块/顺序/离线负载;OLTP 低延迟诉求交给 SSD |
| 介质错误/静默损坏 | page checksum、副本、RAID;企业盘开 ERC |
一句话总结:数据库内核对 HDD 的全部优化,都源于两个事实——机械访问极慢且非原子,以及 “写入返回”默认不保证持久。把它们想清楚,你就能解释为什么早期数据库把”避免随机磁盘访问”当头等大事,为什么 WAL、缓冲池、聚簇、索引、checkpoint 这些机制长这样;而即便换了 SSD,“顺序优于随机""写必须显式落盘”的直觉依然成立,只是代价小了百倍。
参考
- Hard Disk Drive: Mechatronics and Control, Abdullah Al Mamun
- Wikipedia: Hard Disk Drive
- Wikipedia: PRML
- Wikipedia: NCQ
- Wikipedia: Shingled magnetic recording
- Wikipedia: Advanced Format (512e/4Kn)