今日主题#
- 主主题:
DB 打开流程与核心对象关系 - 副主题:
MANIFEST / WAL 恢复如何落到运行时视图
学习目标#
- 讲清
DB::Open()到“数据库可服务”为止的主流程。 - 讲清
VersionSet、ColumnFamilyData、Version、MemTable、SuperVersion在打开阶段分别何时出现、谁连接谁。 - 理解为什么要先恢复 MANIFEST,再回放 WAL,再安装
SuperVersion。 - 给读者一条之后自己继续读 open / recover / manifest 相关源码时不容易迷路的路径。
前置回顾#
- Day 001 已经建立了第一张总图:
DBImpl -> ColumnFamilyData -> SuperVersion -> MemTable / VersionSet。 - Day 002 的任务,是把这张图真正接到启动流程上:
- 打开时先恢复什么
- 再恢复什么
- 最后怎样发布成读路径可见的稳定视图
源码入口#
D:\program\rocksdb\include\rocksdb\db.hD:\program\rocksdb\db\db_impl\db_impl_open.ccD:\program\rocksdb\db\version_set.hD:\program\rocksdb\db\version_set.ccD:\program\rocksdb\db\column_family.hD:\program\rocksdb\db\column_family.ccD:\program\rocksdb\db\db_impl\db_impl.ccD:\program\rocksdb\db\db_impl\db_impl_compaction_flush.cc
它解决什么问题#
DB::Open() 不是“打开几个文件句柄”。
它要解决的其实是 4 个恢复问题:
- 当前数据库最近一次持久化的 LSM 视图是什么。
- 当前有哪些 Column Family,它们各自的
current Version是什么。 - MANIFEST 之后还有哪些 WAL 更新没有落成 SST,需要补回。
- 恢复完成后,前台读线程该拿什么稳定视图开始服务。
一句话概括:
把“磁盘上的历史状态 + WAL 中的增量更新”重建成“当前可服务的运行时对象图”。
它是怎么工作的#
先看打开流程的主链:
flowchart TD
A["DB::Open"] --> B["DBImpl::Open"]
B --> C["Recover"]
C --> D["VersionSet::Recover 读取 CURRENT / MANIFEST"]
D --> E["恢复 ColumnFamilyData / Version / log number"]
E --> F["扫描并回放 WAL"]
F --> G["必要时 WriteLevel0TableForRecovery"]
G --> H["LogAndApplyForRecovery"]
H --> I["创建新 WAL"]
I --> J["为每个 CF 安装 SuperVersion"]
J --> K["MaybeScheduleFlushOrCompaction"]
K --> L["DB 可提供服务"]再换成对象生长过程:
sequenceDiagram
participant API as DB::Open
participant Impl as DBImpl
participant VSet as VersionSet
participant CFD as ColumnFamilyData
participant WAL as WAL Replay
participant SV as SuperVersion
API->>Impl: Open(...)
Impl->>Impl: Recover(...)
Impl->>VSet: Recover(...)
VSet->>CFD: 建立 current Version
Impl->>WAL: RecoverLogFiles(...)
WAL->>CFD: 插回 memtable / 必要时刷成 L0
Impl->>VSet: LogAndApplyForRecovery(...)
Impl->>SV: InstallSuperVersionForConfigChange(...)Day 002 最重要的句子是:
先恢复“磁盘版本视图”,再恢复“最近增量更新”,最后发布“前台读可见视图”。
关键数据结构与实现点#
VersionSet#
- 管数据库级版本元数据。
- 通过 MANIFEST 回放恢复各列族当前
Version。 - 再通过
LogAndApply(...)把恢复过程中形成的新 edit 正式登记进去。
ColumnFamilyData#
- 承接列族级运行时状态。
- 让
current Version、mem、imm最终落到同一个运行时容器里。
SuperVersion#
- 把
mem + imm + current打包成读路径稳定视图。 - 没有它,前台线程虽有版本元数据,但还没有统一的可消费对象。
源码细读#
这次挑 5 个片段。每个片段都回答一个问题,并给出之后继续读源码的路径。
1. DBImpl::Open() 的骨架到底是什么#
先不要被 Open() 里的大量目录、日志、选项细节淹没。先抓主骨架。
关键片段:
// db/db_impl/db_impl_open.cc, DBImpl::Open(...)
RecoveryContext recovery_ctx;
impl->options_mutex_.Lock();
impl->mutex_.Lock();
uint64_t recovered_seq(kMaxSequenceNumber);
s = impl->Recover(column_families, false /* 只读模式 */, ...,
&recovered_seq, &recovery_ctx, can_retry);
if (s.ok()) {
s = impl->CreateWAL(write_options, new_log_number, ... , &new_log);
}
if (s.ok()) {
s = impl->LogAndApplyForRecovery(recovery_ctx);
}
...
SuperVersionContext sv_context(/* create_superversion */ true);
impl->InstallSuperVersionForConfigChange(cfd, &sv_context);这一段说明了什么:
Open()主链并不复杂:Recover- 创建新 WAL
LogAndApplyForRecovery- 安装
SuperVersion
为什么这里重要:
- 以后再回头读
Open()全函数时,能先抓住骨架,不会被各种细枝末节带偏。
读者后续自己读源码时先看哪里:
- 先顺着这 4 个调用点往下读。
- 其他目录创建、统计、异常处理先放后面。
如果只看上面的骨架,还容易把 CreateWAL() 和“给各个列族安装 SuperVersion”当成收尾细节。实际上它们是 open 阶段从“恢复完成”走到“可对外服务”的关键两步。
先看恢复后为什么要立刻创建新的 WAL。
关键片段:
// db/db_impl/db_impl_open.cc, DBImpl::Open(...) 中调用 CreateWAL(...) 后接管当前 WAL 状态的部分
s = impl->CreateWAL(write_options, new_log_number, 0 /* 复用的旧 WAL 编号 */,
preallocate_block_size,
PredecessorWALInfo() /* 前序 WAL 信息 */,
&new_log);
if (s.ok()) {
impl->min_wal_number_to_recycle_ = new_log_number;
}
if (s.ok()) {
InstrumentedMutexLock wl(&impl->wal_write_mutex_);
impl->cur_wal_number_ = new_log_number;
impl->logs_.emplace_back(new_log_number, new_log);
}这一段说明了什么:
Recover()结束并不代表 open 流程结束,数据库还必须为“接下来的新写入”准备好当前 WAL。- 恢复出来的是“过去的状态”;
CreateWAL()负责把系统接回“现在可以继续接收新写入”的状态。
为什么这里重要:
- 如果只记住“打开时会 recover”,会漏掉一个关键事实:
Open()不只是恢复旧世界,还要把新世界的写入入口立起来。 - 后面读写路径时看到
cur_wal_number_、logs_、alive_wal_files_,就知道这些状态在 open 阶段已经接好了。
2. VersionSet::Recover() 为什么是第一步#
这里回答“为什么先恢复 MANIFEST”。
关键片段:
// db/version_set.cc, VersionSet::Recover(...)
std::string manifest_path;
Status s = GetCurrentManifestPath(dbname_, fs_.get(), is_retry,
&manifest_path, &manifest_file_number_);
...
log::Reader reader(nullptr, std::move(manifest_file_reader), &reporter,
true /* 校验 checksum */, 0 /* log 编号 */);
VersionEditHandler handler(
read_only, column_families, const_cast<VersionSet*>(this),
/* 是否跟踪找到和缺失的文件 */ false, no_error_if_files_missing,
io_tracer_, read_options, /* 是否允许不完整但有效的 Version */ false,
EpochNumberRequirement::kMightMissing);
handler.Iterate(reader, &log_read_status);这一段说明了什么:
- 先通过
CURRENT找到当前 MANIFEST。 - 再用
VersionEditHandler顺序回放 MANIFEST 里的 edit。
为什么这里重要:
- RocksDB 先要知道“磁盘世界当前长什么样”,才能继续决定如何处理 WAL 增量。
本节先不展开什么:
VersionEditHandler内部每种 edit 怎么改状态,今天先不细拆。- 这个放到
MANIFEST / VersionEdit / VersionSet那天单独讲。
3. MANIFEST 回放的结果最终落到哪里#
如果只说“恢复了 Version”,还是太抽象,要看到它怎么落到列族对象上。
关键片段 1:
// db/version_set.cc, VersionSet::CreateColumnFamily(...)
ColumnFamilyData* VersionSet::CreateColumnFamily(...) {
...
auto new_cfd = column_family_set_->CreateColumnFamily(
edit->GetColumnFamilyName(), edit->GetColumnFamily(), dummy_versions,
cf_options, read_only);
Version* v = new Version(new_cfd, this, file_options_,
new_cfd->GetLatestMutableCFOptions(), io_tracer_,
current_version_number_++);
...
AppendVersion(new_cfd, v);关键片段 2:
// db/version_set.cc, VersionSet::AppendVersion(...)
void VersionSet::AppendVersion(ColumnFamilyData* column_family_data,
Version* v) {
...
Version* current = column_family_data->current();
...
column_family_data->SetCurrent(v);
v->Ref();这一段说明了什么:
- MANIFEST 回放的结果最终不是停留在临时结构里。
- 它会变成某个
ColumnFamilyData的current Version。
为什么这里重要:
- 这一步把“版本元数据恢复”真正变成了“运行时对象状态恢复”。
读者后续自己读源码时先看哪里:
- 先看
CreateColumnFamily(...)如何创建cfd。 - 再看
AppendVersion(...)如何把Version安到cfd->current()。
4. WAL 回放时为什么可能直接刷成 L0#
这是 Day 002 很容易漏掉但非常关键的一点。
关键片段:
// db/db_impl/db_impl_open.cc, recovery 期间 flush 调度循环与 WriteLevel0TableForRecovery(...) 调用点
while ((cfd = flush_scheduler_.TakeNextColumnFamily()) != nullptr) {
...
VersionEdit* edit = &iter->second;
status = WriteLevel0TableForRecovery(job_id, cfd, cfd->mem(), edit);
if (!status.ok()) {
return status;
}
*flushed = true;
cfd->CreateNewMemtable(*next_sequence - 1);
}这一段说明了什么:
- recovery 期间,WAL 回放插入 memtable 后,如果满足 flush 条件,系统会直接把它刷成 L0 SST。
为什么这里重要:
- 它说明恢复过程不是纯内存重建,而是允许边恢复边整理 LSM 状态。
这段支持了哪个结论:
- WAL 回放恢复的是“增量更新”,而不是简单把所有更新都堆在一个大 memtable 里。
5. 为什么 LogAndApplyForRecovery() 之后还要装 SuperVersion#
先看 recovery 结束时写回 MANIFEST 的动作:
// db/db_impl/db_impl_open.cc, DBImpl::LogAndApplyForRecovery(...)
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
...
Status s = versions_->LogAndApply(recovery_ctx.cfds_, read_options,
write_options, recovery_ctx.edit_lists_,
&mutex_, directories_.GetDbDir());
return s;
}再看安装稳定视图:
// db/column_family.cc, ColumnFamilyData::InstallSuperVersion(...)
void ColumnFamilyData::InstallSuperVersion(...) {
...
new_superversion->Init(this, mem_, imm_.current(), current_, ...);
SuperVersion* old_superversion = super_version_;
super_version_ = new_superversion;
...
ResetThreadLocalSuperVersions();
...
++super_version_number_;
}以及谁负责调用这件事:
// db/db_impl/db_impl_compaction_flush.cc, DBImpl::InstallSuperVersionAndScheduleWork(...)
cfd->InstallSuperVersion(sv_context, &mutex_,
std::move(new_seqno_to_time_mapping));
...
EnqueuePendingCompaction(cfd);
MaybeScheduleFlushOrCompaction();这一组片段说明了什么:
LogAndApplyForRecovery()只是把新版本正式登记到VersionSet/ MANIFEST。- 真正让前台读线程开始消费新状态的,是
InstallSuperVersion(...)。
为什么这里重要:
VersionSet解决“版本元数据现在是什么”。SuperVersion解决“读线程现在拿什么稳定地读”。
读者后续自己读源码时先看哪里:
- 先看
LogAndApplyForRecovery(),确认 recovery 最终如何落成版本状态。 - 再看
InstallSuperVersion(...),确认前台视图是如何被发布的。
最后再补 open 阶段真正“把数据库变成可读态”的那一小段循环。
关键片段:
// db/db_impl/db_impl_open.cc, DBImpl::Open(...) 中遍历各个 Column Family 安装 SuperVersion 的循环
for (auto cfd : *impl->versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
SuperVersionContext sv_context(/* 创建新的 SuperVersion */ true);
impl->InstallSuperVersionForConfigChange(cfd, &sv_context);
sv_context.Clean();
}
}这一段说明了什么:
SuperVersion不是只给默认列族装一次,而是要对当前存活的每个ColumnFamilyData都装好。- 到这一步,
mem + imm + current才真正被逐列族发布成前台读路径可消费的稳定视图。
为什么这里重要:
- 它把 Day 002 的对象关系彻底落到了 open 流程上:
VersionSet恢复的是版本元数据,ColumnFamilyData承接的是列族运行时状态,SuperVersion发布的是每个列族的读视图。 - 读者之后再回源码时,不容易把“恢复出版本状态”和“让前台线程真正能读”混成一件事。
今日问题与讨论#
我的问题#
问题 1:为什么打开流程一定要先恢复 MANIFEST,再回放 WAL?#
- 简答:
- 因为 WAL 增量必须建立在一个已知版本视图之上,否则系统连“当前有哪些列族、每个列族当前版本是什么”都不知道。
- 源码依据:
D:\program\rocksdb\db\version_set.ccD:\program\rocksdb\db\db_impl\db_impl_open.cc
- 当前结论:
- MANIFEST 给出基线,WAL 补上基线之后的增量。
- 是否需要后续回看:
是
问题 2:为什么 recovery 过程中也允许 flush 到 L0?#
- 简答:
- 因为 recovery 本质上是在重新执行写入,memtable 依然可能被写满,系统不能假定恢复期内存无限。
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl_open.cc
- 当前结论:
- recovery 不只是“读日志”,也是“整理状态”。
- 是否需要后续回看:
是
问题 3:为什么 LogAndApplyForRecovery() 后还要安装 SuperVersion?#
- 简答:
- 因为元数据切换完成,不等于前台读视图已经发布完成。
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl_open.ccD:\program\rocksdb\db\column_family.cc
- 当前结论:
VersionSet负责登记版本,SuperVersion负责发布读视图。
- 是否需要后续回看:
否
复习问答回合(2026-04-12)#
- 问题:
DBImpl::Open()为什么不能简单理解成“打开几个文件”?VersionSet::Recover()和 WAL 回放分别在恢复什么?- recovery 期间为什么可能调用
WriteLevel0TableForRecovery()? LogAndApplyForRecovery()和InstallSuperVersion(...)的职责边界是什么?SuperVersion为什么是数据库进入可读态的关键一步?
- 简答:
- 本轮回答对
VersionSet::Recover()、WAL replay、recovery 期间 flush 到 L0 的理解基本正确。 - 关键误解在于把
LogAndApplyForRecovery()说成了“回放日志恢复 memtable / cf version”。更准确地说,WAL replay 才是在恢复增量数据并重建 memtable;LogAndApplyForRecovery()是把 recovery 过程中形成的新 edit 正式落入VersionSet / MANIFEST。 - 对
SuperVersion的回答也还差半步。它不只是 pin 住 memtable / SST,而是把mem + imm + current组合发布成前台读路径可消费的稳定视图。
- 本轮回答对
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl_open.ccD:\program\rocksdb\db\version_set.ccD:\program\rocksdb\db\column_family.ccD:\program\rocksdb\db\db_impl\db_impl_compaction_flush.cc
- 当前结论:
- 本次复习问答结果判定为
fail,原因不是细节遗漏,而是LogAndApplyForRecovery()与 WAL replay 的职责边界出现了关键混淆。 - 在纠正这组边界之前,暂不推进 Day 003。
- 本次复习问答结果判定为
- 是否需要后续回看:
是
复习问答后的补充澄清(2026-04-12)#
- 问题:
WAL replay和LogAndApplyForRecovery()分别负责什么?- 为什么
LogAndApplyForRecovery这个名字看起来像“回放 WAL”,但实际却不是?
- 简答:
WAL replay发生在RecoverLogFiles(...)这条链上,负责读取 WAL record,把WriteBatch里的增量更新重新插回各个 Column Family 的 memtable;如果 recovery 期间 memtable 写满,还可能触发WriteLevel0TableForRecovery(...),把内容刷成 L0。LogAndApplyForRecovery()并不负责“回放 WAL 内容”。它做的是把 recovery 过程中整理出来的VersionEdit列表正式交给versions_->LogAndApply(...),也就是把“这次恢复后形成的新版本元数据”登记进VersionSet / MANIFEST。- 可以把两者粗略理解成:
WAL replay:恢复“数据增量”LogAndApplyForRecovery():提交“版本元数据结果”
- 源码依据:
D:\program\rocksdb\db\db_impl\db_impl_open.ccD:\program\rocksdb\db\version_set.cc
- 当前结论:
- 名字之所以容易误导,是因为这里的
LogAndApply里的Log不是“去读 WAL 日志”,而是“把 edit 记入 descriptor log / MANIFEST”,也就是 VersionSet 那套版本日志语义。 - 所以它如果按更直白但不贴 RocksDB 既有命名体系的方式理解,更接近
LogRecoveredVersionEditsAndApply(),而不是RecoverManifest或ReplayWAL。 RecoverManifest这个名字也不准确,因为恢复 MANIFEST 的事情前面已经由VersionSet::Recover()做过了;LogAndApplyForRecovery()处理的是 recovery 之后新形成的 edit,不是“再去恢复旧 MANIFEST”。
- 名字之所以容易误导,是因为这里的
- 是否需要后续回看:
是
复习问答结果更新(2026-04-12)#
- 问题:
- 在补充澄清之后,Day 002 是否还应继续阻断?
- 简答:
- 不再阻断。
- 原因是关键误解已经纠正:现在已经能区分
WAL replay负责恢复增量数据、LogAndApplyForRecovery()负责提交 recovery 形成的版本 edit。 - 对
SuperVersion的描述虽然还可以再精确到“发布mem + imm + current的稳定可读视图”,但当前回答已经不会对下一章形成关键误导。
- 当前结论:
- 本次复习问答结果从
fail调整为partial。 - Day 002 解除继续学习阻断,但保留
revisit心智,建议在后续读路径章节再次回看。
- 本次复习问答结果从
- 是否需要后续回看:
是
外部高价值问题#
- 今日未引入外部问题。
- 原因:
- Day 002 先把本地源码里的启动骨架讲清楚,比补外部讨论更重要。
常见误区或易混点#
- 误区 1:
DB::Open()只是文件打开动作- 它实际上是在重建运行时对象图。
- 误区 2:
VersionSet恢复完成就等于数据库已经可读- 前台读路径真正依赖的是
SuperVersion。
- 前台读路径真正依赖的是
- 误区 3:WAL 回放只会恢复到 memtable,不会触发 flush
- recovery 期间 memtable 也可能被刷成 L0。
- 误区 4:
SuperVersion只是方便封装- 它是读路径稳定视图、TLS 缓存和旧视图延迟回收的核心枢纽。
设计动机#
为什么打开流程要分成“恢复版本视图”和“恢复增量更新”#
如果把 MANIFEST 和 WAL 混在一起处理,会很难回答两个问题:
- 当前稳定版本边界在哪里。
- 哪些更新只是增量,哪些已经正式进入版本视图。
RocksDB 选择:
- 用 MANIFEST 管“版本真相”
- 用 WAL 管“最近增量”
这样恢复语义更清楚,后续 flush / compaction / snapshot 也更容易围绕同一套版本边界工作。
工程启发#
- 启动恢复时,先划清“持久化基线”和“增量补丁”的边界,比把所有状态混在一起处理更可控。
- 对高并发读系统来说,重要的不只是“现在状态是什么”,还包括“怎样把稳定视图发布给读者”。
- 好的源码讲解不该替代读源码,而该给读者搭桥:先看哪里,为什么看这里,看完应该得到什么结论。
今日小结#
今天最大的收获,是把打开流程压缩成了一条可复述、可回源码验证的主链:
DBImpl::Open()负责总调度。VersionSet::Recover()先恢复 MANIFEST 里的版本视图。- WAL 回放再把增量更新补回 memtable / L0。
LogAndApplyForRecovery()把恢复结果正式登记成新的版本状态。InstallSuperVersion(...)把mem + imm + current发布成前台可读视图。
如果你之后自己回源码,建议就按这 5 步顺着看。
明日衔接#
下一天建议进入:Write Path / WriteBatch / Sequence Number
重点继续看:
D:\program\rocksdb\db\db_impl\db_impl_write.ccD:\program\rocksdb\db\write_thread.hD:\program\rocksdb\db\write_batch.ccD:\program\rocksdb\db\dbformat.h
要带着这几个问题去读:
- 一次写请求如何被分组、分配 sequence、写 WAL、再插入 memtable?
WriteBatch和SequenceNumber如何把原子性与可见性串起来?- recovery 阶段恢复出来的
next_sequence,如何接到实时写路径上?
复习题#
DBImpl::Open()为什么不能简单理解为“打开几个文件”?VersionSet::Recover()和 WAL 回放分别在恢复什么?- recovery 期间为什么可能调用
WriteLevel0TableForRecovery()? LogAndApplyForRecovery()和InstallSuperVersion(...)的职责边界是什么?SuperVersion为什么是数据库进入可读态的关键一步?


