← 返回文章

RocksDB

图解 RocksDB Write Path:WAL、MemTable 与 Flush

沿一次 RocksJava Put 的时间顺序,图解 Writer Group、Sequence Number、WAL、Mutable/Immutable MemTable、Flush、L0 SST 与 Write Stall,回答 Put 返回前后数据到底在哪里。

橙色数据卡进入写入工作区,同时留下蓝灰日志带并落到橙色内存工作台,后台绿色路径把批量记录送往档案文件的编辑风插画
本文目录

应用执行下面这行代码:

db.put(key, value);

方法返回成功时,数据在哪里?

它已经写进 SST 了吗?如果还在内存里,为什么进程崩溃后有机会恢复?WAL 和 MemTable 究竟是谁先写谁?当 MemTable 满了,前台写入是不是必须停下来等文件生成完?

这些问题听起来像是在问一个时间点,实际上混合了至少四个时间点:

  1. Put 刚进入 RocksDB;
  2. 当前写处理已经完成;
  3. MemTable 已经切换为 Immutable;
  4. Flush 生成的 L0 SST 已经发布。

如果不把它们拆开,“Put 成功”“当前可读”“进程崩溃可恢复”和“已经进入 SST”就很容易被误认为同一件事。

先给结论: 一次 Put 会进入 RocksDB 的统一写入处理。默认路径下,更新会记录到 WAL,并应用到 Mutable MemTable;WAL 为尚未 Flush 的更新提供恢复依据,MemTable 让最新状态参与当前读取。Put 返回并不意味着数据已经进入 SST。MemTable 切换后仍然可读,后台 Flush 构建并安全发布 L0 SST,内存与旧 WAL 的职责才逐步结束。

本文按逻辑阶段讲解,不把图画成固定的 C++ 调用栈。RocksDB 支持并发 MemTable Write、Pipelined Write 等路径,版本和选项也会改变内部并行方式。我们关心的是稳定的职责边界,而不是把某个版本的一组函数名背下来。

示例使用 org.rocksdb:rocksdbjni:10.10.1.1,概念边界对照 RocksDB 11.1.2 的公开资料。若你使用其他版本,应再核对对应 Javadoc、Release Notes 与配置默认值。

先看全景:Put 返回只是路径中的一个边界

先把整条 Write Path 放在一张图里。

RocksJava Put 进入统一写入处理,默认记录 WAL 并更新 Mutable MemTable,后续经切换、Flush 和发布成为 L0 SST

Put 返回、MemTable 切换和 Flush 完成是三个不同时间点。

这张图可以分成两部分。

前台写处理负责让本次更新进入数据库当前状态:

RocksJava Put
  → 统一写入入口
  → WAL 恢复记录(默认路径)
  → Mutable MemTable 当前状态
  → 根据写入选项完成返回

图里把 WAL 和 MemTable 并列,是为了强调它们是同一次写处理中的两个职责,而不是“二选一”的两条产品方案。精确的追加、同步和 MemTable Apply 可以因写入模式并发或流水化,本篇不承诺一个适用于所有配置的函数先后表。

后台生命周期负责把内存状态交接给不可变文件:

Mutable MemTable
  → Immutable MemTable
  → 后台 Flush
  → 构建 SST
  → 发布到 L0

所以,当别人问“Put 成功以后数据在哪”,最稳妥的第一句不是“在 WAL”或“在 SST”,而是反问:你关心的是当前读取、崩溃恢复,还是文件持久化?

从 RocksJava 的一次 Put 开始

先看一个尽量完整的最小例子:

import java.nio.charset.StandardCharsets;
import org.rocksdb.Options;
import org.rocksdb.RocksDB;
import org.rocksdb.RocksDBException;
import org.rocksdb.WriteOptions;

public final class WritePathDemo {
    public static void main(String[] args) throws RocksDBException {
        RocksDB.loadLibrary();

        byte[] key = "user:42".getBytes(StandardCharsets.UTF_8);
        byte[] value = "Alice".getBytes(StandardCharsets.UTF_8);

        try (Options options = new Options().setCreateIfMissing(true);
             WriteOptions writeOptions = new WriteOptions()
                 .setDisableWAL(false)
                 .setSync(false);
             RocksDB db = RocksDB.open(options, "./data/write-path-demo")) {
            db.put(writeOptions, key, value);
        }
    }
}

对应的 Maven 依赖可以写成:

<dependency>
  <groupId>org.rocksdb</groupId>
  <artifactId>rocksdbjni</artifactId>
  <version>10.10.1.1</version>
</dependency>

这里有三个容易被“示例太短”掩盖的工程边界。

第一,RocksDB.loadLibrary() 会加载 native 库。RocksJava 不是纯 Java 实现,真正的数据结构、文件和后台线程位于 native RocksDB 中。

第二,OptionsWriteOptionsRocksDB 都持有 native 资源,需要及时关闭。try-with-resources 会按照声明的逆序先关闭数据库,再关闭写选项和数据库选项。生产代码还要确保数据库不会在业务请求仍使用它时被提前关闭。

第三,Key 与 Value 都是字节数组。UTF-8 只是这个示例的编码约定,不是 RocksDB 自动理解的业务类型。

代码中的三个对象分别控制什么?

Options 描述数据库级与默认 Column Family 相关的打开配置,WriteOptions 描述这一次写调用采用的写入边界,RocksDB 则代表已经打开的数据库实例。把它们拆开很重要,因为“数据库怎样组织文件”和“某次写入是否等待 WAL 同步”不是同一层决策。

示例显式写出 disableWAL=falsesync=false,不是说生产环境应该照抄,而是为了让两个开关同时出现在视野里。真正选择时要先回答:这份数据是否可以从上游重放?应用承诺的 RPO 是多少?同步等待增加的尾延迟是否可接受?同一路径中的不同业务写入能否采用不同可靠性等级?

还要注意,成功创建 Java 对象不代表数据目录已经具备正确的运维条件。生产环境至少要确认目录所在文件系统、磁盘空间、权限、备份恢复方案和单进程打开边界。RocksDB 是嵌入式库,不会替应用启动一个独立服务来管理这些生命周期。

如果 db.put 抛出 RocksDBException,调用方不能假设写入成功;如果方法正常返回,也只能按照当前 WriteOptions 与已验证的故障模型解释成功边界。API 返回值本身不会替团队定义业务级“绝不丢失”。

Java 调用经过 JNI 进入 native RocksDB 后,单条 Put 不会绕开数据库的统一写入秩序。它同样要与其他线程、日志、Sequence Number 和 MemTable 协调。

单条 Put 也会进入统一写入入口

从 API 看,put 只包含一条 Key-Value;从写入内核看,它仍是一项需要排队和排序的写操作。

如果同一时刻有三个线程:

Writer A: Put user:42 = Alice
Writer B: Put order:9 = PAID
Writer C: Delete cart:3

RocksDB 需要建立统一的写入顺序,不能让三个线程各自随意追加日志、分配版本并修改共享 MemTable。

为减少每个小请求都单独承担一次固定日志或同步开销,RocksDB 会尝试把兼容的 Writer 组成一个 Write Group。组内请求可以共享底层 WAL 处理,再把各自操作应用到 MemTable。

并发 Writer 进入统一队列,兼容请求可能组成 Group,共享底层 WAL 处理后应用到 MemTable

Group Write 摊薄固定开销,但不能假设所有并发请求必然进入同一批。

这张图故意使用“可能”。是否能进入同一个 Group,会受到 WriteOptions、请求类型、队列时机和内部模式影响。并发抵达也不意味着所有请求必然共享一次同步边界。

还要区分两个经常混在一起的概念:

  • Write Group 是 RocksDB 内部为一轮处理聚合兼容 Writer 的机制;
  • WriteBatch 是调用方显式构造的 API 对象,可以把多条 Put/Delete 作为一个批次提交。

它们都叫“Batch”或“Group”,却不处在同一个抽象层。WriteBatch 的原子性、大小边界和并发语义会在进阶篇单独讨论。

为什么统一入口不能省?

假如每个线程都独立决定自己的 Sequence、独立向同一 WAL 任意写入,再同时修改共享 MemTable,恢复时就很难重建一个一致的新旧顺序。统一入口的价值不只是“排队”,而是把日志记录、顺序分配、内存应用和错误结果放进一个可协调的写入协议。

这也解释了为什么高并发不一定线性提高吞吐。更多 Writer 可以增加组成 Group 的机会,却也可能增加排队、锁竞争、内存分配和设备同步压力。当底层 WAL 或存储设备已经饱和,继续增加业务线程往往只是把等待从 RocksDB 外移到队列里。

观察写入性能时,应把“调用前在业务线程池等待”“进入 RocksDB 后排队”“等待同步”“应用到 MemTable”和“被后台 Stall”区分开。一个端到端 P99 只能告诉你慢了,不能单独证明慢在 WAL、MemTable 还是 Compaction。

写入顺序由谁标记?

假设先后发生:

Put user:42 = Alice
Put order:9 = PAID
Put user:42 = Bob

Alice 与 Bob 的 User Key 相同。即使它们后来分散在不同 MemTable 或 SST,读取也必须知道 Bob 更新。

RocksDB 使用单调递增的 Sequence Number 建立全局写入顺序。可以先把每条操作想象成带着这样的标签:

Seq 101  user:42 = Alice
Seq 102  order:9  = PAID
Seq 103  user:42 = Bob

三次写入被标记为 Seq 101、102、103,同一 user:42 中 Bob 的顺序更新

Sequence Number 先解决“谁更新”,完整可见性规则留给进阶篇。

Sequence Number 不只是一个展示给用户看的时间戳。它会进入 RocksDB 的内部排序与版本可见性判断。同一个 WriteBatch 内多条操作如何分配顺序、Snapshot 为什么只能看到某个 Sequence 上界、Internal Key 怎样编码 User Key、Sequence 和 Value Type,这些都留到 A01。

B03 只保留一个最小模型:

  • 更大的 Sequence 通常代表更新的写入;
  • 当前读视图有一个可见上界;
  • 同一个 User Key 可能保留多个 Sequence;
  • “记录更新”和“记录对某个 Snapshot 可见”不是同一句话。

WAL 与 MemTable:同一次写入的两种职责

有了顺序以后,更新需要同时解决两个问题。

第一个问题是:当前 Get 怎样立刻读到 Alice?

如果 Put 必须等待 SST 文件生成才能可读,前台延迟就会被每次 Flush 的文件构建工作绑住。因此,最新记录先进入 MemTable,读取也会优先检查内存中的新状态。

第二个问题是:Alice 还没有进入 SST,进程崩溃后怎样恢复?

内存结构会随进程消失,所以默认路径还要把更新记录到 WAL。数据库重新打开时,可以重放仍然有效的 WAL,重建尚未被 SST 覆盖的内存状态。

同一次 Put 同时承担 WAL 恢复记录与 MemTable 当前状态两个职责,重启后可重放有效 WAL

WAL 回答怎样恢复,MemTable 回答现在怎样读写。

因此,下面三句话分别是错的:

  • “写了 WAL,所以不需要 MemTable”;
  • “写进 MemTable,所以已经进入 SST”;
  • “sync=false,所以根本没有 WAL”。

默认 disableWAL=false 时,普通写入会记录 WAL。sync 是另一个维度:

WriteOptions 主要变化 不能直接推导出的结论
disableWAL=false 更新参与 WAL 路径 不等于任何断电场景都零丢失
disableWAL=true 跳过 WAL 尚未 Flush 的数据不再拥有同样恢复边界
sync=false 不为本次写入等待同样的同步动作 不等于没有写 WAL
sync=true 返回前请求并等待更强的 WAL 同步 不等于脱离文件系统和硬件就能承诺绝对持久

“崩溃”不是一种故障

讨论 WAL 时必须说明故障模型:

  • Java 进程异常退出;
  • 操作系统崩溃;
  • 主机重启;
  • 突然断电;
  • 存储设备缓存没有掉电保护;
  • 文件系统或介质损坏。

这些故障会跨越不同的缓存和持久化边界。WAL 为恢复提供结构化记录,但“记录已经交给哪个层”“哪个层在故障后仍可靠”还要结合同步调用、文件系统、挂载选项、设备缓存与硬件保障分析。

所以,基础篇只使用这个审慎结论:

在未禁用 WAL 的常见路径中,WAL 为尚未被 SST 覆盖的更新提供恢复依据;sync=true 请求更强的返回前同步边界,精确 RPO 必须放回具体系统验证。

用三个崩溃时刻理解 WAL 的保护窗口

做一个思想实验。数据库最初没有 user:42,应用准备写入 Alice。

场景一:调用刚进入,写处理尚未完成时进程退出。

调用方没有拿到成功结果,新值也可能尚未形成可恢复记录。恢复后是否存在 Alice 不能靠业务意愿猜测,调用方应按失败或结果未知的接口约定处理;需要幂等时,应在 Key、请求标识或上层协议中设计去重边界。

场景二:默认 WAL 路径的 Put 已完成,但 MemTable 尚未 Flush。

旧进程的 MemTable 会消失。重新打开数据库时,RocksDB 读取已经存在的 SST,并重放仍有效的 WAL,把尚未获得 SST 覆盖的更新重新构造成内存状态。恢复完成后,普通 Get 才能再次读到 Alice。

这正是 WAL 最典型的保护窗口:更新已经进入当前数据库状态,但还没有完成从内存到不可变文件的职责交接。

场景三:L0 SST 已经安全发布以后进程退出。

Alice 已经由当前 Version 中的 SST 承担读取职责。相关 WAL 是否立刻可删仍取决于数据库整体状态,尤其是同一日志中是否包含其他 Column Family 尚未 Flush 的数据,但这条记录本身不再只依赖旧 MemTable 才能恢复。

再把场景二改成 disableWAL=true:如果 Alice 尚未 Flush,重启时就没有同等的 WAL 重放依据,它可能丢失。这不是 RocksDB 偷偷忽略了写入,而是调用方主动选择了不同的恢复边界。

思想实验没有解决突然断电时设备缓存是否持久的问题,却先把“进程内存消失”和“存储介质是否真正落稳”拆开了。前者说明为什么需要 WAL,后者说明为什么还要讨论 sync、fsync 和硬件。

Mutable MemTable 怎样接住当前写入?

Mutable MemTable 是当前可写的内存表。Put、Delete 和 Merge 等操作被应用后,会按 Internal Key 的顺序组织在其中。

RocksDB 默认常见的 MemTable Representation 是 Skip List,但也支持 HashSkipList、HashLinkList、Vector 等其他实现。不同实现会改变插入、点查、迭代和内存开销,不能把“MemTable 就是 Skip List”写成永远成立的定义。

对 Write Path 来说,Mutable MemTable 最重要的职责有三项:

  1. 接收新的记录;
  2. 让最新记录立即参与普通读路径;
  3. 为后续有序 Flush 提供可迭代内容。

如果 Alice 已在旧 SST,Bob 刚进入 Mutable MemTable,普通 Get 应先看到 Bob。旧 SST 并没有被原地修改,只是被更高 Sequence 的记录遮蔽。

MemTable 位于 native memory 侧,不能只看 JVM Heap 估算应用内存。Block Cache、Write Buffer、Index/Filter、JNI 对象和后台任务都会占用不同的内存区域。内存治理会在工程篇展开。

Mutable 什么时候变成 Immutable?

活跃 MemTable 达到切换条件后,RocksDB 会把它冻结成 Immutable MemTable。Immutable 的含义是“不再接收新写入”,不是“不能读取”。

与此同时,新的 Mutable MemTable 接管前台写入。

旧 Mutable 切换为可读但不再写入的 Immutable,新 Mutable 接收 Bob,后台独立 Flush 旧表

双缓冲让前台和 Flush 并行,但 Immutable 队列仍有容量边界。

假设旧表中有 Alice,切换后执行 Put Bob:

Immutable: user:42 = Alice
Mutable:   user:42 = Bob

读取会先考虑新的 Mutable,再考虑 Immutable,所以仍返回 Bob。后台可以独立遍历旧表并生成 SST,新写入无需修改正在 Flush 的内容。

不要把 write_buffer_size 当成唯一触发器。MemTable 大小是常见条件,但手动 Flush、全局 Write Buffer Manager、Column Family 间的内存压力和其他内部机制也可能推动切换或 Flush。

这种前后台解耦有一个明确上限:如果 Flush 跟不上,Immutable MemTable 会越积越多。RocksDB 不可能无限占用内存,因此最终必须对前台施加背压。

Flush 怎样让一个 L0 SST 对读取可见?

后台 Flush 会按内部顺序遍历 Immutable MemTable,并使用当前 Table Format 构建 SST。

以常见 Block-Based Table 为直觉,SST 中会包含:

  • 保存记录的 Data Block;
  • 定位数据块的 Index;
  • 可选的 Filter;
  • 压缩、Checksum、Footer 与其他元数据。

文件写出后,还需要完成必要的校验与元数据发布,让新的 SST 进入数据库当前 Version。只有这次职责交接成功,读取才会把它作为 L0 候选。

Immutable MemTable 经有序迭代构建 SST,写出文件并发布元数据后成为可见的 L0 SST

写完文件与让文件对读取可见都完成后,旧内存和日志职责才逐步结束。

这里有三个不能压成一句话的步骤:

  1. 构建: 从 Immutable 的有序内容生成 SST 结构;
  2. 持久化与发布: 让文件及数据库元数据安全衔接;
  3. 回收旧职责: 释放不再需要的 MemTable,并在整体条件允许时处理旧 WAL。

WAL 与 SST 也不是一一对应。一个 WAL 可能包含多个 Column Family 的写入;只要某些相关内存状态尚未获得安全的 SST 覆盖,这段日志就可能仍有恢复职责。

因此,“一个 MemTable Flush 完,所以对应 WAL 立刻删除”不是可靠的通用结论。

自动 Flush 与手动 Flush 应该怎样理解?

正常运行时,RocksDB 会根据内存与写入状态安排后台 Flush。应用也可以显式调用 Flush API,但“能手动触发”不意味着每次 Put 后都应该调用。

如果每条写入后都强制 Flush,就会破坏 MemTable 聚合大量小更新的意义,制造更多小 SST,并把本可批量完成的文件构建工作推到前台节奏附近。是否需要手动 Flush,应该由备份、Checkpoint、关闭流程、测试控制或明确的运维目标驱动,而不是把它误当成 sync=true 的替代品。

两者解决的问题不同:WAL 同步关注尚未 Flush 写入的日志持久化边界;Flush 关注把一批内存状态转换成 SST。Flush 完成也不自动等于目录中所有文件都经过 Compaction,更不代表所有旧版本已经回收。

在线上观察时,还要区分“Flush 已调度”“正在构建文件”和“输出已经安装”。只有最后的元数据发布完成,新的 SST 才加入当前读取版本。一次耗时较长的 Flush 可能慢在排队、设备写入、压缩或元数据收尾,不能只看调用名称下结论。

用四个时刻回答“数据在哪里”

现在可以把开头的问题做成一张时间表。

user:42 从调用进入、WAL和MemTable处理完成、变为Immutable到L0发布,四个时刻的数据位置与恢复依据不同

问“数据在哪里”必须先说明是在 Put 的哪个时刻。

T1:Put 刚进入

请求可能仍在 JNI 调用、写队列或当前 Group 中。此时不能假设新值已经对其他读可见,也不能宣布它已经拥有完整的恢复边界。

T2:当前写处理完成

在默认 WAL 路径的简化模型中,更新已有 WAL 记录并应用到 Mutable MemTable。当前普通读可以从 MemTable 找到新值;如果数据还没进入 SST,恢复依赖有效 WAL。

Put 何时向调用方返回还受 sync、错误、写入模式等影响,所以图中使用“写处理完成”而不是给所有配置强行定义一条机器指令级返回线。

T3:MemTable 已成为 Immutable

旧表不再接收新写入,但仍然可读。新 Mutable 接管更新,后台 Flush 旧表。此时 Alice 可能在 Immutable,Bob 在新 Mutable,而对应 WAL 仍承担未完成交接部分的恢复职责。

T4:L0 SST 已发布

Flush 输出已经安全加入当前 Version,读取可以从 L0 找到相关记录。旧 Immutable 可以释放,相关 WAL 只有在数据库整体条件满足时才可能被回收。

同一条数据从内存到 SST 不是瞬间移动,而是一次带恢复保护的职责交接。

为什么 Put 有时会变慢甚至暂停?

前台写入不断把工作交给后台:MemTable 要 Flush,L0 SST 要继续 Compaction。如果生产速度长期高于消费能力,队列就会增长。

RocksDB 官方 Write Stalls 文档归纳了三类关键压力:

  1. 等待 Flush 的 MemTable 太多;
  2. L0 文件数量太多;
  3. Pending Compaction Bytes 太高。

写入长期快于 Flush 与 Compaction 时,MemTable、L0 文件和待合并字节积压,随后出现 Slowdown 或 Stop

Write Stall 是防止内存、读放大和空间失控的背压机制。

为什么不是让队列继续长?

因为每类积压都会伤害另一条路径:

  • Immutable 过多会占用更多内存,并扩大需要恢复的在途状态;
  • L0 文件过多会增加重叠候选与读放大;
  • Pending Bytes 过高说明未来需要大量后台 I/O 和临时空间;
  • 存储设备长期饱和会同时拉高前台读写尾延迟。

因此 RocksDB 会在某些条件下先降低写入速率,压力更大时暂时停止新写入,让 Flush 或 Compaction 追上。

Stall 不是参数越大越好解决。盲目提高阈值只会允许队列积得更深,可能把一次较早、较短的背压变成更晚、更长的延迟尖峰。正确诊断需要把应用写入速率、Immutable 数、L0 文件、Pending Bytes、后台线程、设备带宽和空间放在同一条时间线上。

具体阈值、指标与调优会放到工程篇;基础篇先记住因果链:

前台写得比后台处理快
  → MemTable / L0 / Pending Bytes 积压
  → 读取、内存与空间风险上升
  → Slowdown / Stop 保护系统

怎样从线上证据定位 Write Path 变慢?

当业务反馈“RocksDB 写入突然慢了”,最容易做错的是立刻改大所有阈值。更可靠的做法是先把一次写入拆成前台、日志、内存和后台四组证据。

第一步:确认慢的是哪里

先比较业务请求总耗时与 db.put 调用耗时。如果前者慢、后者稳定,问题可能在业务队列、序列化或锁;如果 Put 本身的 P99 上升,再继续看 RocksDB 与设备。

同时确认慢的是所有 Column Family,还是只有某一个写入热点。多个 Column Family 可以共享 WAL 与后台资源,但各自 MemTable、L0 和 Compaction 状态不同。只看数据库总吞吐,可能把一个局部积压误判成全局设备故障。

第二步:检查前台写入边界

确认 disableWALsync、WriteBatch 大小和值大小是否发生变化。一次同步小写、一次包含几千条记录的批量写、一个数 MB 的大 Value,虽然都叫 Put 或 Write,前台成本完全不同。

如果同步写比例突然提高,设备同步延迟可能直接进入请求尾延迟;如果 Batch 过大,单个 Writer 会更长时间占据一轮处理,也可能影响其他请求等待。此时只增加业务线程数通常无助于降低单次设备同步时间。

第三步:看 MemTable 与 Flush

观察活跃和 Immutable MemTable 的数量、内存占用、Flush 速率与 Flush 耗时。如果 Immutable 持续增长,说明新表生成得比旧表落盘更快。要继续确认是后台线程不足、压缩耗 CPU、设备写入慢,还是其他 Column Family 抢占了 Flush 资源。

单次出现一个 Immutable 很正常;关键是它能否在负载稳定时回落。诊断应看时间序列,而不是看到某个非零数值就报警。

第四步:看 L0 与 Compaction 债务

如果 MemTable 能顺利 Flush,但 L0 文件数与 Pending Compaction Bytes 持续上涨,瓶颈已经从“内存到 L0”移动到“L0 向后续层级整理”。此时 Put 变慢可能是 Compaction Stall,而不是 WAL 或 MemTable 插入变慢。

还要把设备读写带宽、I/O 利用率、队列深度和尾延迟放在一起。Compaction 同时读旧 SST、写新 SST;设备显示高吞吐不一定代表健康,它也可能已经饱和,导致前台 WAL 与后台文件 I/O 互相干扰。

第五步:做收支判断,而不是只看阈值

最终要回答两个速率:前台每秒制造多少待 Flush、待 Compaction 数据,后台每秒能稳定消化多少。如果前者长期更大,扩大缓冲区只会延迟 Stall 到来的时间;只有减少输入、增加有效后台能力、改善文件组织或更换资源边界,才能让系统回到稳定状态。

一份有用的故障记录至少应保留负载变化前后几分钟的应用延迟、写入字节、MemTable 状态、L0 文件、Pending Bytes、Flush/Compaction 吞吐、磁盘空间和设备延迟。把这些曲线对齐,通常比复制一份“最佳 RocksDB 参数”更快接近根因。

六个常见误区

误区一:Put 返回时数据一定已经在 SST

数据通常先由 MemTable 提供当前可读状态。SST 要等 MemTable 切换并由后台 Flush 构建、发布。

误区二:WAL 写完以后,RocksDB 才开始碰 MemTable

WAL 和 MemTable 是同一次写处理中的恢复与查询职责。并发写、Pipelined Write 等模式会改变内部重叠方式,不应把基础图当成所有版本的固定函数调用栈。

误区三:sync=false 就没有 WAL

是否写 WAL 由 disableWAL 控制;sync 关注是否为该写入等待更强同步动作。这是两个不同开关。

误区四:启用 WAL 就能保证所有断电场景零丢失

进程、内核、文件系统、设备缓存与介质是不同层级。可靠性承诺必须明确同步配置、故障模型和硬件条件。

误区五:MemTable 只由 write_buffer_size 触发切换

它是重要条件之一,但全局内存预算、手动 Flush 和其他机制也会影响切换与 Flush。

误区六:Write Stall 是 RocksDB 随机卡住

Stall 通常是可观察的后台积压结果,是保护内存、读取和空间边界的背压。真正的问题是为什么后台长期跟不上。

再走一遍 Put user:42

把所有机制压回一次写入:

  1. RocksJava 把 Put("user:42", "Alice") 通过 JNI 送入 native RocksDB;
  2. 写操作进入统一队列,可能与兼容 Writer 组成 Group;
  3. RocksDB 为写入建立 Sequence Number 顺序;
  4. 默认路径记录 WAL,并把记录应用到 Mutable MemTable;
  5. 根据 WriteOptions 和处理结果,Put 完成并返回;
  6. Mutable 达到切换条件后成为仍可读的 Immutable;
  7. 新 Mutable 接管后续写入,后台 Flush 旧表;
  8. Flush 构建 SST 并通过元数据发布到 L0;
  9. 旧 MemTable 释放,相关 WAL 在整体条件满足时才可能回收;
  10. 若 Flush 与 Compaction 长期慢于写入,背压会让 Put 减速或暂停。

一句话总结:

WAL 保护尚未完成文件交接的更新,MemTable 服务当前状态,Flush 把内存职责安全交给 L0 SST;Put 返回只是这条生命周期上的前台边界。

下一篇 B04《图解 RocksDB Read Path:一次 Get 如何找到最新值》,我们会从写入留下的多个位置出发,回答 Get("user:42") 怎样先看 Mutable 与 Immutable,再用文件范围、Bloom Filter、Index Block 和 Block Cache 缩小 SST 候选。

参考资料