你每天都在和存储打交道,写数据到 MySQL、读 Redis 缓存、往 S3 上传文件。但你有没有想过:为什么 SSD 比 HDD 快这么多?为什么数据库用 B+ 树而不是哈希表?为什么 LSM 树写快读慢?为什么云数据库要计算存储分离?
这些问题的答案,都要从最底层的物理器件说起。一台计算机里散布着好几种存储器件,它们的物理原理完全不同,由此带来的速度、容量、成本、是否断电丢失的差异,构成了整个存储系统的地基。本文先把这几种器件的物理特性和工作机制讲清楚,再逐层向上,看操作系统、数据结构、存储引擎和分布式系统如何在这一地基上搭建。
一、从物理器件开始
1.1 五种常见存储器件
打开一台计算机,从 CPU 内部到机箱里的硬盘,存储器件大致可以分成五类:
| 器件 | 位置 | 是否断电丢失 | 速度 | 容量 | 每 GB 成本 |
|---|---|---|---|---|---|
| 寄存器 | CPU 内 | 是 | 极快 | 几百字节 | 极高 |
| SRAM | CPU 内(缓存) | 是 | 极快 | KB 到 MB 级 | 高 |
| DRAM | 主板(内存条) | 是 | 快 | GB 级 | 中 |
| ROM/Flash | 主板、硬盘 | 否 | 较慢 | MB 到 TB 级 | 低 |
| 磁盘/SSD | 机箱 | 否 | 慢 | TB 级 | 极低 |
这张表的关键不是具体数字,而是趋势:越靠近 CPU 越快、越小、越贵,越远离 CPU 越慢、越大、越便宜。后面所有存储设计的取舍,都在围绕这条规律打转。
下面逐个看它们的物理原理。理解了原理,那些性能数字就不是死记硬背的常数,而是顺理成章的结果。
1.2 寄存器:CPU 的口袋
寄存器是 CPU 内部数量最少的存储单元,存放当前正在运算的数据和指令地址。它由**触发器(Flip-Flop)**构成,一个触发器存一个比特。
要理解触发器,得先说一个更基本的概念:锁存器。锁存器是一种”能记住一个比特”的最小电路。它的关键特性叫双稳态(bistable):电路有两个稳定状态,分别表示 0 和 1,不靠外力维持就能停在其中一个,相当于一个带闸门的窄走廊,人(数据)进来后关上闸门,就一直待在里面。改变状态要给一个电信号”推一下”,速度只受晶体管开关速度限制。
现代寄存器里实际用的是 D 锁存器和 D 触发器(D 是 Data 的缩写)。D 锁存器比朴素锁存器多一个”闸门”输入,叫使能信号(常用 CLK,即时钟)。使能有效时闸门打开,输出 Q 跟随输入 D 变化;使能无效时闸门关上,输出保持上一刻的值。简化真值表如下:
| 使能 E | 输入 D | 输出 Q |
|---|---|---|
| 1(有效) | 0 | 0(跟随) |
| 1(有效) | 1 | 1(跟随) |
| 0(无效) | 任意 | 保持不变 |
D 锁存器有个麻烦:使能有效期间,输入一抖动输出就跟着变,可能在一个使能周期内翻转多次。寄存器要的是”每个时钟周期采样一次”,所以实际用的是 D 触发器,它在时钟信号跳变的那一瞬间(边沿)采样,跳变前后的输入变化都不影响输出。这种”只在跳变沿采样”的特性叫边沿触发,区别于 D 锁存器的电平敏感(使能电平高低决定是否透明)。
访问延迟在 1 纳秒以内,和一次 CPU 时钟周期差不多。代价是体积大,一个触发器要占好几个晶体管,CPU 里塞不下太多,所以寄存器只有几十到几百个,每个几十到几百字节。它离运算单元最近,几乎是零延迟访问,但断电即失。
1.3 SRAM:CPU 缓存用的快存储
SRAM(Static Random Access Memory,静态随机存取存储器)和寄存器同根同源,都靠双稳态电路存比特。区别在于它不直接用完整的 D 触发器,而是把电路精简成 6 个晶体管(6T 单元):两个反相器交叉耦合构成双稳态节点(还记得 1.2 的窄走廊吗?就是它),再加两个晶体管做访问开关。省掉了触发器里的时钟控制部分,密度高得多。它同样不需要刷新,只要有电数据就在,所以叫”静态”。
代价是仍然比 DRAM 贵。一个 6T 单元的面积约是 DRAM 1T1C 单元的 4 到 6 倍,同样面积能塞下的比特少,所以容量受限,常用于 CPU 的 L1/L2/L3 缓存,加起来通常只有几 KB 到几十 MB。
L1、L2、L3 之所以分成三级,是容量、速度和成本的权衡:L1 最小最快,每个 CPU 核心独占;L3 最大最慢,多核共享。越靠上层越贵,所以容量越小,命中率靠”局部性”(最近用过的数据短期内还会用,相邻的数据很快会被访问)来维持。
1.4 DRAM:主内存的主力
DRAM(Dynamic Random Access Memory,动态随机存取存储器)的存储单元更简单,一个电容加一个晶体管就存一个比特。电容有电荷表示 1,无电荷表示 0。
这种结构带来两个物理约束,决定了 DRAM 的全部特性:
第一,电容会漏电。 电荷会随时间慢慢流失,所以 DRAM 必须定期”刷新”,把即将流失的电荷重新充满,否则数据就没了。按 JEDEC 标准,DDR4/DDR5 要求每一行在 64ms 内(高温下 32ms)刷新一次,控制器以约 7.8μs 的间隔逐行刷新,把成千上万行轮询一遍刚好填满这个窗口。这就是”动态”的含义。刷新要占用总线时间,也是 DRAM 比 SRAM 慢的原因之一。
第二,电容充电放电需要时间。 读取时要把电容电荷放大成数字信号,写回时要重新充电,单次访问延迟在 100 纳秒量级,比 SRAM 慢一个数量级。
但它的优势压倒一切:单元小、密度高、便宜。一根内存条轻松做到几十 GB,这是 SRAM 做不到的。主内存用 DRAM,是因为容量和成本的平衡点落在这里。
“随机存取”(Random Access)指的是访问任意地址的时间相同,与数据所在位置无关。磁盘就不是随机存取,磁头要先移动到对应磁道,访问时间取决于位置。这个区别后文会反复出现。
1.5 ROM 与 Flash:断电不丢的存储
前面三类器件都断电即失。ROM(Read-Only Memory)和 Flash(闪存)则能持久保存数据,靠的是物理层面的电荷囚禁。
ROM 早期真的只能写一次,后来发展出可擦写的 EPROM、EEPROM。Flash 是 EEPROM 的一种变体,通过大批量擦写降低成本,成了今天 U 盘、SD 卡、SSD、手机存储的主流。
NOR Flash 支持随机读取,可以像内存一样直接执行代码,但容量小、贵,常用于存 BIOS 和固件。NAND Flash 密度高、便宜,用于大容量存储,SSD 内部就是一片片 NAND 闪存。
NAND 的物理特性有个关键约束:写入前必须先擦除,且擦除以”块”为单位。一个擦除块通常是 2 到 4MB,里面包含很多个 4KB 到 16KB 的页。读和写以页为单位,擦除却要以块为单位。这就引出了 SSD 的所有麻烦,后文会专门讲。
1.6 磁盘与 SSD:海量持久存储
NAND Flash 做成 SSD 后,它和传统机械磁盘 HDD 一起构成了”持久化大容量存储”这一层。两者的物理原理截然不同。
HDD(机械磁盘) 靠磁记录。盘片高速旋转,磁头悬在盘面上方,通过改变盘片上磁性材料的磁化方向来写 0/1,通过感应磁场变化来读。访问数据时,磁头要先移动到对应磁道(寻道),再等目标扇区转到磁头下方(旋转延迟),最后才是读写。这两步都是机械运动,毫秒级,慢得刺眼。
SSD 靠电子信号,没有机械运动。读写以闪存页为单位,延迟在微秒级,比 HDD 快两个数量级。但前面说过,SSD 写入前要擦除整块,于是需要一个”闪存转换层(FTL)“把逻辑地址映射到物理地址,还要做磨损均衡、垃圾回收。这套机制带来的写放大问题,是 SSD 设计的核心难点。
1.7 把器件串成存储层级
把上面五类器件按速度从快到慢排开,就得到了教科书里的”存储层级金字塔”:
每一层之间都有数量级的性能差异:寄存器到 DRAM 差 100 倍,DRAM 到 SSD 又差 100 倍,SSD 到 HDD 再差 100 倍。存储系统设计的核心矛盾就是速度与容量的权衡:L1 缓存比 HDD 快约 1000 万倍(1ns 对 10ms),容量却只有其几亿分之一。
一个有用的思维实验(脱胎自 Jeff Dean 的经典类比):如果把一次 CPU 时钟周期算作 1 秒(即时间放大 10 亿倍),那么寄存器和 L1 缓存访问约 1 秒,L2 约 4 秒,主内存访问约 1 到 2 分钟(100ns),NVMe SSD 访问约 3 到 30 小时(10 到 100μs),HDD 访问约 4 个月(10ms)。差距大到难以用同一种时间感来衡量。这就是为什么存储系统设计的永恒目标是减少慢速层的访问次数,缓存、预取、压缩、索引,所有策略都为这个目标服务。
1.8 延迟数量级对比
把各层延迟摆在一起,建立直觉:
| 存储介质 | 延迟 | 吞吐量 | 每 GB 成本 | 随机访问 |
|---|---|---|---|---|
| 寄存器 | ~1 ns | ~1 TB/s | ~$10,000 | 极快 |
| L1 Cache | ~1 ns | ~1 TB/s | ~$10,000 | 极快 |
| L2 Cache | ~4 ns | ~500 GB/s | ~$5,000 | 极快 |
| L3 Cache | ~10 ns | ~200 GB/s | ~$1,000 | 极快 |
| DRAM | ~100 ns | ~50 GB/s | ~$10 | 快 |
| NVMe SSD | ~10 μs | ~7 GB/s | ~$0.5 | 较快 |
| SATA SSD | ~100 μs | ~0.5 GB/s | ~$0.3 | 一般 |
| HDD | ~10 ms | ~0.2 GB/s | ~$0.03 | 慢 |
| 网络存储 | ~100 μs–1 ms | ~10 GB/s | ~$0.1 | 取决于协议 |
这张表是理解后续所有内容的锚点。后文提到的”为什么 B+ 树要矮胖""为什么 LSM 树把随机写转顺序写""为什么缓冲池是数据库的核心”,答案都能在这张表里找到根据。
二、物理特性如何决定设计策略
2.1 顺序与随机的鸿沟
物理器件有一个跨越所有层级的特性:顺序访问远快于随机访问。在 HDD 上尤其夸张,顺序读写约 200MB/s,随机读写只有 0.5MB/s,差 400 倍。原因是磁头一旦定位,连续读就不需要再寻道,而随机访问每次都要重新寻道加等旋转。
SSD 没有机械运动,但顺序仍然优于随机。原因是顺序写入能让 SSD 内部按页连续编程,减少擦除块的碎片化,垃圾回收和磨损均衡的负担也小。随机写入会让 FTL 频繁触发垃圾回收,写放大飙升。
| 特性 | 顺序 I/O | 随机 I/O |
|---|---|---|
| HDD 性能 | ~200 MB/s | ~0.5 MB/s(400 倍差距) |
| SSD 性能 | ~5 GB/s | ~0.1 GB/s(50 倍差距) |
| 适用场景 | 日志、Compaction、备份 | 索引查找、点查 |
| 优化策略 | 批量写入、Group Commit | 缓存、预取、索引 |
理解了这一点,再看 LSM 树(Log-Structured Merge-Tree,一种写优化的存储结构,第四章详解)的核心思想就很自然:把随机写入转化为顺序写入。用户写入先进内存的 MemTable,积累后顺序刷盘成 SSTable。代价是读取时要检查多个层级,这叫读放大。这种取舍不是拍脑袋,是物理特性逼出来的。
2.2 SSD 延迟分解与写放大
SSD 的访问延迟并非铁板一块,内部有多个环节:
| SSD 延迟环节 | 典型耗时 | 说明 |
|---|---|---|
| NVMe 命令提交 | ~1 μs | SQ/CQ 队列操作 |
| FTL 映射查找 | ~2 μs | 逻辑地址到物理地址 |
| 闪存页读取 | ~25 μs | 单页 16KB |
| 闪存页编程 | ~300 μs | 单页写入 |
| 擦除块擦除 | ~3 ms | 整块 2 到 4MB |
| GC 搬迁延迟 | ~50 μs | 后台垃圾回收引入 |
读延迟约 25 到 50 μs,但写延迟因垃圾回收和擦除操作波动极大。FTL 的写放大(Write Amplification, WA)通常在 1.5 到 3 倍之间,意味着用户写 1MB,闪存实际写入 1.5 到 3MB,SSD 寿命因此缩短。
2.3 从物理事实到设计原则
前面两节讲了顺序/随机的鸿沟和 SSD 的写放大,但物理器件的影响远不止这两点。把第一节的几个关键物理事实拎出来,逐条对应到它们逼出的设计原则,能得到一张贯穿全文的”翻译表”:
| 物理事实 | 带来的约束 | 设计原则 | 后文对应 |
|---|---|---|---|
| SRAM/DRAM 断电即失 | 数据可能在崩溃时丢失 | 必须有持久化机制:WAL、日志、快照 | 文件系统日志、数据库 WAL |
| HDD 寻道慢,SSD 随机读也慢 | 随机定位代价高 | 引入索引避免全量扫描:B+ 树、哈希、LSM | 存储引擎架构 |
| NAND 写前必须整块擦除 | 不能原地改写 | FTL 地址映射、垃圾回收、写放大控制 | SSD 延迟分解 |
| 快层小而贵,慢层大而便宜 | 容量与速度倒挂 | 多级缓存、预取、利用局部性 | 页缓存、缓冲池、CPU 缓存 |
| 顺序 I/O 远快于随机 I/O | 随机写入是性能杀手 | 把随机写转顺序写:WAL、LSM 树 | 写路径、LSM 树 |
这张表的价值在于,它说明了一个容易被忽略的事实:后文那些看似复杂的设计,没有一个是为了炫技而存在的。文件系统做日志、数据库做 WAL、Redis 做 AOF,根上都是同一件事,回应”断电即失”这一条约束;B+ 树、哈希索引、LSM 树,回应的是”随机定位慢”这一条。把这些映射记住,读到任何存储系统的设计决策时,都能反向追问一句:它到底在回应哪条物理约束?
把这些原则归纳一下,就是贯穿后文的几条通用策略:
# 存储层级的核心设计策略strategies = { "缓存(Caching)": "将热数据放在更快的层级,减少慢速层访问", "预取(Prefetching)": "预测未来需要的数据,提前加载到快速层", "批处理(Batching)": "将多个小 I/O 合并为大 I/O,摊销延迟开销", "压缩(Compression)": "减少数据量,降低传输和存储开销", "索引(Indexing)": "用额外的空间换取查找速度,避免全量扫描", "顺序化(Sequential)": "将随机 I/O 转化为顺序 I/O,利用磁盘带宽",}CPU 缓存用预取,文件系统用缓冲,数据库用索引和缓冲池,LSM 树用顺序化和批处理。理解了物理特性,这些设计就不再是孤立的知识点,而是同一组约束的不同应用。
物理特性讲完了,下一节看操作系统如何在这套地基上搭抽象,让应用不必直接面对磁道和闪存页。
三、操作系统如何封装物理器件
物理器件的原始接口不适合应用直接使用。没人想自己管理磁道和扇区,也没人想手动往 DRAM 里搬数据。操作系统在物理器件和应用之间搭了多层抽象,让上层只需关心”读写文件""分配内存”,不必知道底层是 HDD 还是 SSD。
3.1 从磁盘到文件:文件系统
文件系统是操作系统提供的第一层存储抽象。它把磁盘上的扇区组织成文件和目录,用户通过文件名访问数据,不用关心数据实际落在哪个磁道、哪个闪存页。
这层抽象做了几件关键的事:
- 空间管理:把磁盘分成固定大小的块(通常 4KB),用位图或 B+ 树记录哪些块空闲、哪些已分配。文件不需要连续存放,文件系统维护逻辑块到物理块的映射。
- 命名与组织:目录本质是一棵树,每个目录项记录文件名到 inode 的映射。inode 存放文件的元数据(大小、权限、时间戳)和数据块的位置。
- 崩溃一致性:断电或崩溃可能让磁盘上的数据处于半写状态。文件系统用**日志(Journal)或写时复制(Copy-on-Write)**来保证崩溃后能恢复到一致状态。ext4 的日志模式(data=ordered)是 Linux 上最常见的方案。
- 缓存与预读:Linux 的**页缓存(Page Cache)**把最近读写过的磁盘块缓存在 DRAM 里。下次访问同一块数据时,直接从内存返回,不用再碰磁盘。预读机制在顺序访问时提前加载后续块。
页缓存就是第一节那个思维实验的工程体现:磁盘太慢(4 个月 vs 1 秒),所以把热数据搬到 DRAM 里(1 到 2 分钟),用空间换时间。Linux 上 free -h 输出里的 “buff/cache” 就是页缓存占用的内存。
常见的 Linux 文件系统对比:
| 文件系统 | 特点 | 适用场景 |
|---|---|---|
| ext4 | 日志模式成熟,稳定 | 通用场景,大多数 Linux 发行版默认 |
| XFS | 大文件性能好,并行 IO 强 | 数据库、大文件存储 |
| Btrfs | 写时复制,快照,子卷 | 需要快照和校验的场景 |
| ZFS | 端到端校验,RAID-Z | 数据完整性要求高的存储服务器 |
3.2 从文件到块设备:块层与 IO 栈
文件系统之下是 Linux 的块层(Block Layer),负责管理 IO 请求的调度和分发。应用发出一个 read() 系统调用,数据经过的路径大致如下:
- VFS(Virtual File System):统一接口层,让应用不管底层是 ext4 还是 NFS,都用同一套
open/read/write/close。 - 页缓存:命中则直接返回,不命中则向块层提交 IO 请求。
- 块层:Linux 5.x 之后默认用 blk-mq(多队列块层),把 IO 请求分到多个硬件队列,减少锁竞争。IO 调度器在这里决定请求的发送顺序。
- 设备驱动:把块层提交的请求翻译成具体的硬件命令(NVMe 命令、SATA 命令等)。
io_uring 是 Linux 5.1 引入的新型 IO 接口,通过共享环形缓冲区减少系统调用开销,在高并发 IO 场景下比传统 read/write 快数倍。数据库和存储引擎正在逐步迁移到 io_uring。
3.3 从内存到进程:虚拟内存与页表
操作系统管理 DRAM 的方式同样值得了解。每个进程看到的不是物理内存,而是虚拟内存:一段连续的地址空间,由 CPU 的内存管理单元(MMU)通过页表映射到物理内存。
这层抽象带来的好处:
- 隔离:每个进程有独立的地址空间,不会互相干扰
- 按需分配:虚拟地址可以映射到物理页,也可以暂时不映射(访问时触发缺页中断,再从磁盘加载)
- 换出(Swap):物理内存不够时,操作系统把不活跃的页写回磁盘(Swap 区),腾出 DRAM 给更需要的进程。这相当于用磁盘扩展内存,代价是慢几千倍
虚拟内存是存储层级的另一种体现:DRAM 是快速层,磁盘 Swap 是慢速层,操作系统用缺页中断和页替换算法在两层之间搬运数据,和页缓存的逻辑一模一样。
3.4 直接 IO 与异步 IO:绕过操作系统的捷径
页缓存和缓冲对大多数应用是好事,但有些场景需要绕过它:
- O_DIRECT:跳过页缓存,应用直接和磁盘交互。数据库(InnoDB、PostgreSQL)常用这种方式,自己管理缓冲池,避免操作系统页缓存和数据库缓冲池的双重缓存。
- DAX(Direct Access):针对持久内存(如 Intel Optane),应用可以直接通过内存指令访问持久化设备,不需要经过块层和页缓存。
- 异步 IO:传统
read/write是同步阻塞的,应用必须等 IO 完成。io_uring和libaio提供异步接口,应用提交 IO 请求后继续做别的事,完成后再处理结果。
这些”捷径”本质是应用在说:我比操作系统更懂自己的访问模式,让我自己来。
操作系统搭好了抽象,但到了需要保证 ACID、追求极限吞吐的场景,应用仍要绕过它自己来。下一节看存储引擎如何在这两者之间取舍。
四、存储引擎架构
4.1 通用存储引擎架构
无论具体实现如何,大多数存储引擎都遵循相似的分层架构:
这个架构里每一块都能对应回物理特性:MemTable 在 DRAM 里所以快,SSTable 落在 SSD 上,WAL 用顺序写规避随机写的代价,Block Cache 用 DRAM 缓存热点数据减少 SSD 访问。
4.2 B 树引擎 vs LSM 树引擎
这是存储系统中最根本的设计取舍。在给出对比之前,先从物理层推一遍为什么会有这个取舍。
B+ 树要设计得矮胖(每个节点扇出大、层数少),直接原因是 HDD 的 10ms 寻道。树每深一层,点查就多一次磁盘 IO,HDD 上每多一层就是 10ms 的代价,三四层下去延迟就不可接受了。所以 B+ 树用大页(16KB)塞下成百上千个键,把树高压到 3 到 4 层,这是用空间和写放大换读取层数。代价是写入时要原地更新数据页,页满了还要分裂,一次写可能牵动多页的随机 IO,写放大中等但每次写都是随机定位。
LSM 树走相反的路。它的写入不原地改数据,而是追加到内存的 MemTable,攒够一批后顺序刷盘成 SSTable。这样写入对磁盘来说全是顺序写,规避了 SSD 内部”写前擦除整块”的约束,因为顺序写让 FTL 能整块连续编程,垃圾回收负担最小。代价是读取时要查 MemTable 加多层 SSTable,每多一层就多一次潜在的磁盘读,这就是读放大。为了压读放大又得频繁 Compaction 重写数据,进一步推高写放大。
两个设计都想优化某一面,但都被物理层钉死了成本:HDD 的寻道决定了 B+ 树不能深,SSD 的擦除块决定了随机写很贵,DRAM 的容量-成本倒挂决定了缓存层塞不下所有数据。这就是 RUM 猜想的物理来源:Read-cost、Update-cost、Memory-cost 三者不可兼得,优化其中两个必然牺牲第三个,因为它们背后是同一组物理约束的不同投影。
| 维度 | B 树引擎(InnoDB) | LSM 树引擎(RocksDB) |
|---|---|---|
| 写入方式 | 原地更新(In-place Update) | 追加写入(Out-of-place Update) |
| 写放大 | 中等(页面分裂、日志) | 较高(Compaction 重写) |
| 读放大 | 低(O(log N) 树遍历) | 较高(多层级查找) |
| 空间放大 | 中等(页面碎片) | 较高(过期数据待 Compaction) |
| 最佳场景 | 读多写少 | 写多读少 |
| 并发控制 | Latch + 锁 | Copy-on-Write |
| 典型系统 | InnoDB、PostgreSQL | RocksDB、LevelDB、Cassandra |
B 树和 LSM 树的取舍就是 RUM 猜想的体现:Read-cost、Update-cost、Memory-cost 三者不可兼得,优化其中两个必然牺牲第三个。
五、应用层如何使用存储
存储引擎给出了磁盘上组织数据的方案,但应用不会直接对着引擎 API 写代码。数据库、缓存、对象存储这些中间件在引擎之上又封了一层,针对自己的工作负载做了专门优化。这一节看它们各自怎么用存储、怎么取舍。
操作系统提供了文件和块设备的抽象,但应用层通常不会直接读写裸文件。数据库、缓存、消息队列等中间件在操作系统之上又搭了一层,针对自己的工作负载做了专门优化。
5.1 数据库:最复杂的存储使用者
数据库是存储系统最苛刻的用户。它要保证持久性(写入的数据断电不丢)、一致性(数据始终满足约束)、隔离性(并发事务互不干扰)、原子性(事务要么全部生效要么全部回滚),合称 ACID。
为了满足这四个要求,数据库在操作系统之上做了大量工作:
- 缓冲池(Buffer Pool):数据库自己管理一块 DRAM 缓存,缓存磁盘上的数据页。和操作系统的页缓存功能类似,但数据库更了解自己的访问模式,能用更精细的替换策略(如 LIRS 而非简单 LRU)。这就是为什么数据库通常用
O_DIRECT绕过页缓存,避免双重缓存浪费内存。 - WAL(Write-Ahead Log):每次修改数据前,先把修改记录写到日志里。日志用顺序写,比随机写数据页快得多。崩溃后通过重放日志恢复,这就是 ARIES 算法的核心思想。
- B+ 树或 LSM 树:数据在磁盘上的组织方式。B+ 树读快写慢,LSM 树写快读慢,选择哪种取决于工作负载。
- MVCC(多版本并发控制):通过保存数据的多个版本,让读操作不阻塞写操作,写操作不阻塞读操作。
数据库的每个设计决策都能对应回物理层的约束。WAL 用顺序写,因为 SSD/HDD 的顺序性能远优于随机。缓冲池用 DRAM 缓存,因为 DRAM 比磁盘快 100 到 10000 倍。B+ 树要矮胖(扇出大),因为每多一层就多一次磁盘 IO,10ms 的 HDD 寻道代价承受不起深树。
5.2 键值缓存:把 DRAM 用到极致
Redis、Memcached 这类系统把数据全部放在 DRAM 里,追求极低的读写延迟(亚毫秒级)。它们不需要关心磁盘 IO,但受限于 DRAM 的容量和成本。
Redis 的持久化方案正好体现了存储层级的取舍:
| 方案 | 原理 | 性能影响 | 数据安全 |
|---|---|---|---|
| RDB 快照 | 定期把内存数据 fork 后写磁盘 | fork 瞬间有内存压力 | 两次快照间的数据可能丢 |
| AOF 追加日志 | 每次写操作追加到日志文件 | 每次写多一次磁盘 IO | 取决于 fsync 频率 |
| AOF + RDB 混合 | AOF 重写时用 RDB 格式 | 折中 | 兼顾恢复速度和安全 |
AOF 的 appendfsync everysec 配置意味着最多丢 1 秒数据。这是用持久性换性能的经典权衡。
5.3 对象存储:面向应用的持久化抽象
应用不需要总是通过文件系统访问持久存储。对象存储(S3、MinIO)提供了更简单的接口:PUT 一个对象、GET 一个对象、DELETE 一个对象,没有目录、没有 inode、没有块设备。
这种简化的代价是功能受限:没有部分写入,没有文件锁,一致性模型有限。但换来的是几乎无限的水平扩展和极低的运维成本。
对象存储的典型应用模式:
- 数据湖:原始数据以 Parquet/ORC 格式存放在对象存储上,计算引擎(Spark、Presto)按需读取
- 备份与归档:数据库备份、日志归档直接写入 S3,利用生命周期策略自动迁移到更便宜的存储层
- 静态资源:图片、视频、前端资源通过 CDN 加速的对象存储分发
5.4 应用选型的决策框架
面对不同存储方案,可以按以下维度决策:
| 决策维度 | 问自己的问题 | 指向 |
|---|---|---|
| 延迟要求 | 能接受毫秒级还是必须亚毫秒? | 亚毫秒 → DRAM(Redis);毫秒级 → SSD(数据库);百毫秒级可接受 → HDD/对象存储 |
| 持久性要求 | 断电能丢吗? | 不能丢 → WAL + 刷盘(数据库);能丢几秒 → AOF everysec(Redis);完全不要持久化 → 纯内存 |
| 数据规模 | GB 级还是 TB/PB 级? | GB → 单机数据库;TB → 分布式数据库;PB → 对象存储 + 数据湖 |
| 访问模式 | 点查、范围扫描、还是批量分析? | 点查 → B+ 树/哈希;范围扫描 → B+ 树;批量分析 → 列存 |
| 一致性要求 | 需要强一致还是最终一致即可? | 强一致 → 关系数据库;最终一致 → 对象存储、NoSQL |
每一条决策都能对应回第一节那张延迟表和物理特性。存储选型不是”哪个更先进”,而是”哪个约束恰好匹配你的需求”。
六、存储系统的性能模型
6.1 三个放大因子
存储系统的性能可以用三个”放大因子”来衡量:
| 放大因子 | 定义 | 公式 | 影响 |
|---|---|---|---|
| 写放大(Write Amplification) | 实际写入磁盘的数据量 / 用户写入的数据量 | WA = 磁盘写入量 / 用户写入量 | SSD 寿命、写入延迟 |
| 读放大(Read Amplification) | 一次用户读取触发的磁盘 I/O 次数 | RA = 磁盘读取次数 / 用户读取次数 | 读取延迟 |
| 空间放大(Space Amplification) | 磁盘上实际占用的空间 / 用户数据大小 | SA = 磁盘占用 / 用户数据量 | 存储成本 |
# 写放大示例:LSM 树的 Compaction# 假设用户写入 1MB 数据user_write = 1 # MB
# Level 0 → Level 1: 重写 1MB# Level 1 → Level 2: 重写 10MB# Level 2 → Level 3: 重写 100MBtotal_disk_write = user_write * (1 + 10 + 100) # = 111MB
write_amplification = total_disk_write / user_write # = 111xprint(f"写放大: {write_amplification}x")6.2 写路径的关键步骤
一个写入请求从应用到持久化,经历以下关键步骤:
# 写路径关键步骤及延迟估算write_path_steps = { "1. WAL 写入": "~10 μs(NVMe 顺序写)", "2. MemTable 更新": "~0.1 μs(内存跳表插入)", "3. WAL 刷盘(提交)": "~10 μs(fsync/fdatasync)", "4. MemTable → SSTable": "~1 ms(后台 Flush,异步)", "5. Compaction": "~10 ms 到 1 s(后台,异步)",}# 同步延迟:步骤 1+2+3 ≈ 20 μs(用户感知的写入延迟)# 异步延迟:步骤 4+5 在后台执行,不阻塞用户写入把每个步骤的延迟对照第一节那张延迟表,就能看懂为什么这样设计:WAL 必须落盘才能保证持久性,但用顺序写把延迟压到 10 μs;MemTable 放在 DRAM 里,所以内存操作只需 0.1 μs;Compaction 太慢,只能丢到后台异步做。
6.3 存储性能基准
以下是常见存储操作的延迟参考值,最初来自 Jeff Dean 整理的 “Latency Numbers Every Programmer Should Know”,已更新至 2024 年前后硬件水平:
| 操作 | 延迟 | 说明 |
|---|---|---|
| L1 缓存引用 | 1 ns | CPU 内部 |
| L2 缓存引用 | 4 ns | CPU 内部 |
| 互斥锁加/解锁 | 20 ns | 纯内存操作 |
| 主内存引用 | 100 ns | DRAM 访问 |
| 压缩 1KB 数据(ZSTD) | 3 μs | 单核 |
| 从 SSD 顺序读 1MB | 10 μs | NVMe |
| 网络往返(同机房) | 100 μs | 1Gbps |
| 从 SSD 顺序读 1MB | 200 μs | SATA |
| 从磁盘顺序读 1MB | 5 ms | HDD |
| 磁盘寻道 | 10 ms | HDD 随机访问 |
6.4 新兴存储技术:CXL 与计算存储
传统存储层级存在一个根本矛盾:DRAM 容量有限,SSD 延迟太高。CXL(Compute Express Link)和计算存储正在打破这一限制:
| 技术 | 原理 | 延迟 | 适用场景 |
|---|---|---|---|
| CXL 内存 | 通过 CXL 协议扩展远端 DRAM | ~200–500 ns | 大内存数据库、缓存 |
| CXL Cache | 远端设备缓存一致性 | ~100–200 ns | 多节点共享缓存 |
| 计算存储 | SSD 内嵌处理器,就近计算 | 省去数据搬运 | 压缩、过滤、扫描 |
| 近存计算 | DRAM 旁挂 FPGA/ASIC | ~50 ns | 向量化聚合 |
CXL 内存往返延迟比本地 DRAM 高 2 到 5 倍,不适合作为 Buffer Pool 的主存储。更适合存放冷数据或作为内存扩展池。计算存储目前生态尚不成熟,仅在大规模扫描场景(如数据湖过滤)有显著收益。
前面几节是纵向的因果推导,从物理到应用到性能。下一节换一个视角,把存储系统横向分类,建立一张可以快速对照的知识图谱。
七、存储系统的分类
7.1 按介质分类
易失与持久的分界线,正是第一节讲的物理原理:SRAM 和 DRAM 靠电荷维持状态,断电即失;ROM/Flash 靠电荷囚禁,HDD 靠磁化方向,都能持久保存。
7.2 按访问模式分类
| 访问模式 | 特点 | 典型系统 | 数据结构 |
|---|---|---|---|
| 块存储 | 固定大小块,随机读写 | 本地磁盘、EBS、Ceph RBD | 块设备 |
| 文件存储 | 目录树结构,POSIX 语义 | ext4、NFS、CephFS | 树形目录 |
| 对象存储 | 扁平命名空间,HTTP 访问 | S3、MinIO、Ceph RGW | 哈希索引 |
| 键值存储 | 简单 KV 接口,高性能 | RocksDB、LevelDB | LSM/B+ 树 |
| 文档存储 | 结构化文档,灵活 Schema | MongoDB | B 树 |
7.3 按读写模式分类
| 读写模式 | 特点 | 典型场景 | 代表引擎 |
|---|---|---|---|
| 读优化 | 读取快,写入需原地更新 | OLTP 点查 | InnoDB(B+ 树) |
| 写优化 | 写入快,读取需合并 | 日志、时序 | RocksDB(LSM 树) |
| 分析优化 | 批量扫描快,点查慢 | OLAP 分析 | Parquet(列存) |
| 混合型 | 读写均衡 | HTAP | TiKV(LSM + 缓存) |
八、从单机到分布式的演进
前面几节都局限在一台机器内。但单机的存储总有天花板:磁盘容量有限、单节点吞吐有限、一台机器挂了数据就可能丢。分布式的本质,是用网络(微秒到毫秒级延迟)替代部分本地 IO,换取水平扩展能力和容错能力。回头看第一节那张延迟表,网络存储的延迟(100μs 到 1ms)落在 DRAM 和 SSD 之间,这正是分布式系统能够成立的物理前提:网络虽然比本地内存慢,但还没慢到不可接受,用它换多机容量和可靠性是划算的。
8.1 存储系统的演进路线
8.2 演进的核心驱动力
| 阶段 | 核心驱动力 | 解决的问题 | 引入的新问题 |
|---|---|---|---|
| 本地磁盘 | 数据持久化 | 断电不丢失 | 单盘故障等于数据丢失 |
| RAID | 数据冗余 | 单盘故障容忍 | 重建慢、扩展性差 |
| 分布式文件系统 | 水平扩展 | 容量/性能瓶颈 | 一致性、复杂性 |
| 对象存储 | 海量非结构化数据 | 规模与成本 | 一致性模型弱 |
| 计算存储分离 | 弹性伸缩 | 资源利用率 | 网络延迟、一致性 |
| 资源分解 | 细粒度资源分配 | 资源浪费 | 硬件复杂性 |
每一步演进都在解决前一步引入的问题。单盘怕故障就有了 RAID,RAID 扩展性差就有了分布式,分布式一致性难管就有了对象存储的简化模型,云上要弹性就有了计算存储分离。理解这条线索,比记住任何具体产品都重要。
8.3 存储系统的 CAP 取舍
分布式存储系统必须在 CAP 三者之间做出取舍:
| 系统 | 一致性(C) | 可用性(A) | 分区容忍(P) | 取舍说明 |
|---|---|---|---|---|
| Ceph RADOS | 强一致 | 可用 | 容忍 | 用 Quorum 保证一致性 |
| MinIO | 强一致 | 可用 | 容忍 | 纠删码 + Quorum |
| S3 | 强一致(读后写) | 高可用 | 容忍 | 2020 年后提供强一致读,仍以可用性优先设计 |
| Cassandra | 可调一致 | 高可用 | 容忍 | 可调 Quorum 级别 |
| Aurora | 强一致 | 可用 | 容忍 | Quorum + 6 副本 |
前面讲的都是原理和取舍,最后一节落到工具上,看看怎么在实际系统里观察这些存储层级的行为。
九、实战:观察存储层级
9.1 Linux 存储栈观察工具
# 查看块设备信息lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,ROTA# ROTA=1 表示旋转设备(HDD),ROTA=0 表示非旋转设备(SSD)
# 查看 I/O 调度器cat /sys/block/sda/queue/scheduler
# 查看页面缓存统计cat /proc/meminfo | grep -i "cache\|buffer"
# 使用 iostat 观察 I/O 统计iostat -x 1 5
# 使用 perf 观察 I/O 延迟分布perf stat -e 'block:block_rq_issue,block:block_rq_complete' -a sleep 109.2 数据库存储行为观察
-- 观察 InnoDB 状态SHOW ENGINE INNODB STATUS\G
-- 观察 Buffer Pool 命中率SHOW STATUS LIKE 'Innodb_buffer_pool_read%';-- Innodb_buffer_pool_read_requests /-- (Innodb_buffer_pool_read_requests + Innodb_buffer_pool_reads)
-- 观察页面刷新统计SHOW STATUS LIKE 'Innodb_data_written';SHOW STATUS LIKE 'Innodb_os_log_written';-- 观察缓冲区统计SELECT sum(heap_blks_read) AS heap_read, sum(heap_blks_hit) AS heap_hit, sum(heap_blks_hit) / NULLIF(sum(heap_blks_hit + heap_blks_read), 0) AS cache_hit_ratioFROM pg_statio_user_tables;
-- 观察 WAL 写入量SELECT pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) AS wal_written;9.3 存储延迟直方图
# 使用 bcc 工具观察 I/O 延迟分布# biolatency - I/O 延迟直方图/usr/share/bcc/tools/biolatency 1 10
# 输出示例:# usecs : count distribution# 0 -> 1 : 0 | |# 2 -> 3 : 0 | |# 4 -> 7 : 0 | |# 8 -> 15 : 12 |****** |# 16 -> 31 : 45 |********************** |# 32 -> 63 : 89 |****************************************|# 64 -> 127 : 23 |*********** |# 128 -> 255 : 5 |** |十、小结
回头看全文的线索:触发器和电容决定了 SRAM 快但贵、DRAM 便宜但要刷新;电荷囚禁让 Flash 断电不丢但必须整块擦除;磁记录让 HDD 容量大但寻道慢。这些物理事实一层一层往上投影:文件系统用日志对付崩溃一致性,页缓存用 DRAM 挡在磁盘前面,数据库用 WAL 把随机写变顺序写,Redis 用 AOF 在持久性和延迟之间取折中,对象存储用简化一致性换水平扩展。每一层设计都不是凭空发明,而是在回答同一组物理约束提出的同一组问题:怎么减少慢速层的访问,怎么在速度和容量之间找到平衡点。
参考资料
- Latency Numbers Every Programmer Should Know - Jeff Dean 整理的各级存储延迟数量级,本文延迟表与思维实验的来源
- Amazon S3 强一致性公告(2020) - S3 提供 PUT/DELETE 后强一致读的官方说明
- CMU 15-445/645 - 数据库系统课程,存储引擎部分
- 《操作系统导论》(OSTEP) - 文件系统与存储的操作系统视角
- 《Database Internals》 - Alex Petrov,存储引擎与分布式系统实现
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






