mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
10137 字
27 分钟
计算机存储原理全景
2025-05-19

你每天都在和存储打交道,写数据到 MySQL、读 Redis 缓存、往 S3 上传文件。但你有没有想过:为什么 SSD 比 HDD 快这么多?为什么数据库用 B+ 树而不是哈希表?为什么 LSM 树写快读慢?为什么云数据库要计算存储分离?

这些问题的答案,都要从最底层的物理器件说起。一台计算机里散布着好几种存储器件,它们的物理原理完全不同,由此带来的速度、容量、成本、是否断电丢失的差异,构成了整个存储系统的地基。本文先把这几种器件的物理特性和工作机制讲清楚,再逐层向上,看操作系统、数据结构、存储引擎和分布式系统如何在这一地基上搭建。

一、从物理器件开始#

1.1 五种常见存储器件#

打开一台计算机,从 CPU 内部到机箱里的硬盘,存储器件大致可以分成五类:

器件位置是否断电丢失速度容量每 GB 成本
寄存器CPU 内极快几百字节极高
SRAMCPU 内(缓存)极快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(有效)00(跟随)
1(有效)11(跟随)
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。

graph LR CPU["CPU 核心"] --> REG["寄存器<br/>几十到几百字节"] REG --> L1["L1 缓存 SRAM<br/>~64KB"] L1 --> L2["L2 缓存 SRAM<br/>~256KB"] L2 --> L3["L3 缓存 SRAM<br/>~8MB"] L3 --> DRAM["主内存 DRAM<br/>~128GB"] style REG fill:#ffcdd2 style L1 fill:#ffe0b2 style L2 fill:#fff9c4 style L3 fill:#c8e6c9 style DRAM fill:#bbdefb

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,是因为容量和成本的平衡点落在这里。

Note

“随机存取”(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 把器件串成存储层级#

把上面五类器件按速度从快到慢排开,就得到了教科书里的”存储层级金字塔”:

graph TB subgraph 存储层级["存储层级金字塔 — 越往上越快越贵,越往下越慢越便宜"] REG["寄存器<br/>~1ns / 几百字节"] SRAM["SRAM 缓存 L1/L2/L3<br/>~1-10ns / ~8MB"] DRAM["主内存 DRAM<br/>~100ns / ~128GB"] SSD["SSD<br/>~10-100μs / ~2TB"] HDD["HDD 磁盘<br/>~10ms / ~20TB"] TAPE["磁带<br/>~10s / ~PB 级"] end REG --> SRAM --> DRAM --> SSD --> HDD --> TAPE

每一层之间都有数量级的性能差异:寄存器到 DRAM 差 100 倍,DRAM 到 SSD 又差 100 倍,SSD 到 HDD 再差 100 倍。存储系统设计的核心矛盾就是速度与容量的权衡:L1 缓存比 HDD 快约 1000 万倍(1ns 对 10ms),容量却只有其几亿分之一。

Note

一个有用的思维实验(脱胎自 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 μsSQ/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 里。下次访问同一块数据时,直接从内存返回,不用再碰磁盘。预读机制在顺序访问时提前加载后续块。
Note

页缓存就是第一节那个思维实验的工程体现:磁盘太慢(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() 系统调用,数据经过的路径大致如下:

graph LR APP["应用 read/write"] --> VFS["VFS 虚拟文件系统"] VFS --> FS["文件系统 ext4/XFS"] FS --> PC["页缓存 Page Cache"] PC -->|"命中"| APP2["直接返回"] PC -->|"未命中"| BLK["块层 Block Layer"] BLK --> DRV["设备驱动"] DRV --> DEV["存储设备 HDD/SSD"]
  • VFS(Virtual File System):统一接口层,让应用不管底层是 ext4 还是 NFS,都用同一套 open/read/write/close
  • 页缓存:命中则直接返回,不命中则向块层提交 IO 请求。
  • 块层:Linux 5.x 之后默认用 blk-mq(多队列块层),把 IO 请求分到多个硬件队列,减少锁竞争。IO 调度器在这里决定请求的发送顺序。
  • 设备驱动:把块层提交的请求翻译成具体的硬件命令(NVMe 命令、SATA 命令等)。
Tip

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_uringlibaio 提供异步接口,应用提交 IO 请求后继续做别的事,完成后再处理结果。

这些”捷径”本质是应用在说:我比操作系统更懂自己的访问模式,让我自己来。

操作系统搭好了抽象,但到了需要保证 ACID、追求极限吞吐的场景,应用仍要绕过它自己来。下一节看存储引擎如何在这两者之间取舍。

四、存储引擎架构#

4.1 通用存储引擎架构#

无论具体实现如何,大多数存储引擎都遵循相似的分层架构:

graph TB subgraph 存储引擎架构 CLIENT["客户端请求"] --> API["API 层<br/>Get/Put/Delete/Scan"] API --> WRITE["写路径<br/>WAL → MemTable → SSTable"] API --> READ["读路径<br/>Cache → Bloom → 磁盘"] WRITE --> WAL["WAL 日志<br/>崩溃恢复保障"] WRITE --> MEM["MemTable<br/>内存中的有序结构"] MEM --> FLUSH["Flush<br/>内存到磁盘"] FLUSH --> SST["SSTable 文件<br/>磁盘上的有序数据"] SST --> COMPACT["Compaction<br/>合并与清理"] READ --> CACHE["Block Cache<br/>热点数据缓存"] READ --> BLOOM["Bloom Filter<br/>减少无效查找"] READ --> SST WAL --> RECOVERY["崩溃恢复<br/>ARIES 算法"] end

这个架构里每一块都能对应回物理特性: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、PostgreSQLRocksDB、LevelDB、Cassandra

B 树和 LSM 树的取舍就是 RUM 猜想的体现:Read-cost、Update-cost、Memory-cost 三者不可兼得,优化其中两个必然牺牲第三个。

graph TB subgraph RUM 猜想["RUM 猜想:三者不可兼得"] R["Read 优化<br/>B+ 树"] --- TRADE1["写放大 + 空间碎片"] U["Update 优化<br/>LSM 树"] --- TRADE2["读放大 + 空间放大"] M["Memory 优化<br/>紧凑编码"] --- TRADE3["读放大 + 写放大"] end R -.->|"不可兼得"| U U -.->|"不可兼得"| M M -.->|"不可兼得"| R

五、应用层如何使用存储#

存储引擎给出了磁盘上组织数据的方案,但应用不会直接对着引擎 API 写代码。数据库、缓存、对象存储这些中间件在引擎之上又封了一层,针对自己的工作负载做了专门优化。这一节看它们各自怎么用存储、怎么取舍。

操作系统提供了文件和块设备的抽象,但应用层通常不会直接读写裸文件。数据库、缓存、消息队列等中间件在操作系统之上又搭了一层,针对自己的工作负载做了专门优化。

5.1 数据库:最复杂的存储使用者#

数据库是存储系统最苛刻的用户。它要保证持久性(写入的数据断电不丢)、一致性(数据始终满足约束)、隔离性(并发事务互不干扰)、原子性(事务要么全部生效要么全部回滚),合称 ACID。

为了满足这四个要求,数据库在操作系统之上做了大量工作:

  • 缓冲池(Buffer Pool):数据库自己管理一块 DRAM 缓存,缓存磁盘上的数据页。和操作系统的页缓存功能类似,但数据库更了解自己的访问模式,能用更精细的替换策略(如 LIRS 而非简单 LRU)。这就是为什么数据库通常用 O_DIRECT 绕过页缓存,避免双重缓存浪费内存。
  • WAL(Write-Ahead Log):每次修改数据前,先把修改记录写到日志里。日志用顺序写,比随机写数据页快得多。崩溃后通过重放日志恢复,这就是 ARIES 算法的核心思想。
  • B+ 树或 LSM 树:数据在磁盘上的组织方式。B+ 树读快写慢,LSM 树写快读慢,选择哪种取决于工作负载。
  • MVCC(多版本并发控制):通过保存数据的多个版本,让读操作不阻塞写操作,写操作不阻塞读操作。
Note

数据库的每个设计决策都能对应回物理层的约束。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: 重写 100MB
total_disk_write = user_write * (1 + 10 + 100) # = 111MB
write_amplification = total_disk_write / user_write # = 111x
print(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 nsCPU 内部
L2 缓存引用4 nsCPU 内部
互斥锁加/解锁20 ns纯内存操作
主内存引用100 nsDRAM 访问
压缩 1KB 数据(ZSTD)3 μs单核
从 SSD 顺序读 1MB10 μsNVMe
网络往返(同机房)100 μs1Gbps
从 SSD 顺序读 1MB200 μsSATA
从磁盘顺序读 1MB5 msHDD
磁盘寻道10 msHDD 随机访问

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向量化聚合
Warning

CXL 内存往返延迟比本地 DRAM 高 2 到 5 倍,不适合作为 Buffer Pool 的主存储。更适合存放冷数据或作为内存扩展池。计算存储目前生态尚不成熟,仅在大规模扫描场景(如数据湖过滤)有显著收益。

前面几节是纵向的因果推导,从物理到应用到性能。下一节换一个视角,把存储系统横向分类,建立一张可以快速对照的知识图谱。

七、存储系统的分类#

7.1 按介质分类#

graph TB subgraph 存储介质分类 MEMORY["内存存储<br/>Redis / Memcached<br/>断电丢失 / 极快"] SSD["SSD 存储<br/>NVMe / SATA<br/>持久化 / 较快"] HDD["磁盘存储<br/>HDD<br/>持久化 / 慢"] TAPE["磁带存储<br/>LTO-9<br/>归档 / 极慢"] end MEMORY --> VOLATILE["易失性存储"] SSD --> PERSISTENT["持久化存储"] HDD --> PERSISTENT TAPE --> PERSISTENT

易失与持久的分界线,正是第一节讲的物理原理:SRAM 和 DRAM 靠电荷维持状态,断电即失;ROM/Flash 靠电荷囚禁,HDD 靠磁化方向,都能持久保存。

7.2 按访问模式分类#

访问模式特点典型系统数据结构
块存储固定大小块,随机读写本地磁盘、EBS、Ceph RBD块设备
文件存储目录树结构,POSIX 语义ext4、NFS、CephFS树形目录
对象存储扁平命名空间,HTTP 访问S3、MinIO、Ceph RGW哈希索引
键值存储简单 KV 接口,高性能RocksDB、LevelDBLSM/B+ 树
文档存储结构化文档,灵活 SchemaMongoDBB 树

7.3 按读写模式分类#

读写模式特点典型场景代表引擎
读优化读取快,写入需原地更新OLTP 点查InnoDB(B+ 树)
写优化写入快,读取需合并日志、时序RocksDB(LSM 树)
分析优化批量扫描快,点查慢OLAP 分析Parquet(列存)
混合型读写均衡HTAPTiKV(LSM + 缓存)

八、从单机到分布式的演进#

前面几节都局限在一台机器内。但单机的存储总有天花板:磁盘容量有限、单节点吞吐有限、一台机器挂了数据就可能丢。分布式的本质,是用网络(微秒到毫秒级延迟)替代部分本地 IO,换取水平扩展能力和容错能力。回头看第一节那张延迟表,网络存储的延迟(100μs 到 1ms)落在 DRAM 和 SSD 之间,这正是分布式系统能够成立的物理前提:网络虽然比本地内存慢,但还没慢到不可接受,用它换多机容量和可靠性是划算的。

8.1 存储系统的演进路线#

graph LR subgraph 单机时代 LOCAL["本地磁盘<br/>ext4 / XFS"] RAID["RAID<br/>冗余与性能"] end subgraph 分布式时代 DFS["分布式文件系统<br/>Ceph / Gluster"] OBJ["对象存储<br/>MinIO / S3"] end subgraph 云原生时代 CLOUD["计算存储分离<br/>Aurora / Neon"] DISAGG["资源分解<br/>远端内存 / 智能网卡"] end LOCAL --> RAID --> DFS --> OBJ --> CLOUD --> DISAGG

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 10

9.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_ratio
FROM 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 在持久性和延迟之间取折中,对象存储用简化一致性换水平扩展。每一层设计都不是凭空发明,而是在回答同一组物理约束提出的同一组问题:怎么减少慢速层的访问,怎么在速度和容量之间找到平衡点。

参考资料#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

计算机存储原理全景
https://blog.souloss.cn/posts/storage/storage-overview/
作者
Souloss
发布于
2025-05-19
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时