2.2 核心组件:WAL、MemTable 与 SSTable


2.2 核心组件:WAL、MemTable 与 SSTable

本节摘要:WAL、MemTable、SSTable 不是三件并列的工具,而是一条流水线上互相咬合的三个工位:WAL 用顺序日志买崩溃安全,MemTable 用内存结构买写入速度,SSTable 用分块与索引买读取效率。本节逐个拆开它们的内部构造,讲清衔接的时序纪律,最后用一个六步案例把「一次写入的一生」走完整。

一笔转账引发的追问

先给自己出一道题:应用调用 Put 写入一笔转账记录,引擎返回成功,二十毫秒后服务器断电。重启之后,这笔记录在不在?

答案取决于一个严格的时序:引擎承诺「成功」之前,这笔操作的证据是否已经落到了磁盘上。 如果证据在内存里还没落盘,那这笔写入就是空头支票。WAL(预写日志)存在的全部意义,就是把这道题的答案钉死为「在」:任何内存修改发生之前,操作的完整记录必须先以顺序追加的方式写入日志并按承诺刷盘。它不关心数据最终怎么组织,只保证「承诺过的事一定留痕」。这就是三个工位的第一条纪律:先落日志,再动内存。

第二条纪律关于衔接:WAL 不是无限长的,它以 MemTable 的生命周期为刻度回收——一张内存表对应一段日志,表被安全刷成 SST 后,那段日志才可回收。日志与内存表是共生体,谁也不能单方面提前退场。

MemTable:内存里的有序舞台

内存表的默认实现是跳表,这个选择把三件事一次做对:并发(无锁插入,多写线程不打架)、有序(天然支持范围遍历,为刷盘与扫描省去排序)、内存友好(节点分配模式规整,碎片少)。每个节点除了键和值,还带着序列号——内存表因此天然是多版本的:同一个键的几次写入并存,新旧靠序列号排队。

内存管理交给专门的内存池:只分配不逐个释放,表销毁时整段回收。这个看似粗暴的策略把内存管理的成本压到常数级——对每秒百万次写入的路径来说,省下的正是最贵的那部分开销。

容量到线,舞台换场。冻结的细节上一节已讲,这里补一笔 flush 时的「结构化增值」:冻结表在刷成 SST 的路上并不是原样照搬,后台会顺手构建数据块索引、生成布隆过滤器、按策略压缩数据块。内存表原本只是「有序的写入快照」,经过这一趟加工,变成「磁盘上可直接高效查询的结构」。加工是增量成本,收益归读取路径——又是一笔典型的跨路径转账。

SSTable:磁盘上的自描述结构

SST 文件是一套自带索引与校验的磁盘结构,切分层级如下:

组成部分 职责 类比
数据块 存放压缩后的键值对,块内有序 正文
索引块 记录每个数据块的边界键与位置 目录
过滤器块 布隆过滤器,快速否定「本文件没有这个键」 速查卡
元块 文件级统计与属性 版权页
脚注 指向索引与过滤器的指针,附校验和 封底勘误表

一次点查在这个结构里的路径是固定的:先查过滤器,判定「绝对没有」就直接返回;否则查索引块,二分定位到唯一的数据块;把数据块读进内存解析,找到键;全程校验和护航。注意这个设计的精妙处:磁盘访问次数有硬上界——最多一次过滤器读、一次索引读、一次数据块读,再多就没有了。读取延迟的可预测性就是从这里来的。

SST 还有两个常被忽视的元数据:键范围(最小键与最大键)和序列号范围(本文件内最老与最新的序列号)。键范围让「哪些文件可能与目标键相关」成为一次内存比较就能回答的问题;序列号范围让压缩在挑拣旧版本时不用翻内容。文件因此是「自描述」的——元数据足够丰富,读取与压缩都能先看档案再动手。

图2-2 三工位流水线:一次写入的完整接力

图2-2 三工位流水线:一次写入的完整接力

六步走完一次写入

把前面的构造拼成一个具体案例。设写缓冲为常规配置,观察键 user:123 的一次更新:

第一步 · 领号:引擎给它分配序列号,比如 1001。从此它在所有版本排序中有了唯一座次。

第二步 · 留痕:操作被序列化成一条日志记录——序列号、键、值、操作类型四样俱全——追加进 WAL,按当前写选项的要求刷盘。这步走完,崩溃就再也抹不掉它。

第三步 · 入住:记录插进活跃内存表的跳表。此刻起,任何不带快照限制的读取都能看到新值——虽然磁盘上还什么都没有。

第四步 · 换场:写入持续,内存表达到容量线,被冻结为只读;新表无缝接班,前台毫无感觉。冻结表进入后台刷盘队列。

第五步 · 铸型:后台线程拾起冻结表,构建索引块与过滤器块、压缩数据块,写成一个 L0 层 SST 文件,并在版本账本上登记这个新文件。对应的那段 WAL 从此可回收。

第六步 · 归位:随后的某次压缩把这个文件与邻居合并、下沉到更深的层。user:123 的新值随大流沉入 L1、L2,直到某天被一次 Get 从深处捞起,或被更新的版本覆盖后清退出局。

六步里有一步值得你多想三十秒:第五步完成之前,数据同时在 WAL 和内存表里存在两份;第五步完成之后,SST 顶替了日志的位置。持久性的载体就是这样换手的——这也解释了为什么日志的回收必须以刷盘成功为前提,提前回收就是自毁保险单。

一次中断的推演:三个组件如何互相兜底

机制的分量要在事故里称。推演一次最普通的故障:进程在写入进行到一半时被强行终止,内存清零,磁盘完好。

打开库的瞬间,三份资产的状态各不相同:日志里躺着全部已确认的写入,包括崩溃前最后几毫秒;内存表随进程消失,一个字节都不剩;磁盘文件完好,但只包含到某次刷盘为止的数据。恢复的对账规则因此简单而精确——重放日志里比文件更新的部分,把崩溃前留在内存里的那一截补回来。日志为什么必须先落、内存表为什么敢放在易失的内存里、文件为什么敢慢慢刷,三个设计在这一刻互为因果:日志的严格换来了内存的放纵,内存的放纵换来了写入的速度,文件的从容换来了前台的安静。

推演再往前走一步:如果日志在崩溃时写了半条呢?半条记录的校验码对不上,恢复时自动截断——只有完整落盘的记录才有资格被重放。这就是「确认返回」四个字的重量:凡是引擎答过一声成功的写入,一定在日志里有完整的一条;凡是没答的,丢了也不算违约。持久承诺的边界就画在这里,多一分是浪费,少一分是谎言。

本节要点

  • WAL 的唯一使命是把「成功」变成可兑现的承诺,纪律只有一条:先落日志,再动内存;
  • MemTable 用跳表同时买断并发、有序与内存友好;内存池的「只分不还」把管理成本压到常数;
  • flush 不是照搬而是增值:索引、过滤器、压缩都在这一步生成,收益归读取路径;
  • SST 是自描述结构:键范围与序列号范围让读取和压缩都能先看档案再动手;
  • 点查的磁盘访问次数有硬上界,这是读延迟可预测性的结构来源;
  • 持久性载体会换手:内存表刷成 SST 后,对应日志才能回收。

数据落进了 SST,但「字节层面长什么样、版本由谁记账」还是黑盒。下一章进数据模型与存储格式的内部。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U