# RecoveryContext
- 存储 recover 过程中新生成的 VersionEdits
- 核心成员:
- **`edit_lists_`**:`vector<vector<VersionEdit *>>`
- 外层 vector 代表 cf
- 内层 vector 代表某个 cf 的 edits
- **`cfds_`**:`vector<ColumnFamilyData *>`
- 跟 `edit_lists_` 对应的 cfd
- **`existing_data_files`**:`vector<std::string>`
- Open时目录下的所有 SST 和 Blob 文件
- 收集后添加到 `SstFileManagerImpl`
- **`is_new_db_`**:`bool`,是否是在创建一个新 DB
- **UpdateVersionEdit(cfd, edit)**
- 追加一个 VersionEdit
---
# WAL Recovery
## WALRecoveryMode
- 枚举类型:
- **`kTolerateCorruptedTailRecords = 0`**
- 容忍==任意== wal 的 last record 是不完整的,也容忍 preallocation 导致的末尾 zero bytes
- 出现其他情况的 corruption 则 DB 打开失败
- **`kAbsoluteConsistency = 1`**
- 不容忍任何 corruption,否则打开失败
- **`kPointInTimeRecovery = 2`**
- 默认选项,顺序回放 wal 的 records ,直到遇到 corruption后停止回放
- 如果中间某个 wal 出现 corruption,则会停止回放==后面==的 wals
- 
- IO 错误的处理: 打开失败
- PR:[Fail point-in-time WAL recovery upon IOError reading WAL #6963](# Fail point-in-time WAL recovery upon IOError reading WAL #6963)
- 更安全的方式:开启 [track_and_verify_wals_in_manifest](https://github.com/facebook/rocksdb/wiki/Track-WAL-in-MANIFEST)
- **`kSkipAnyCorruptedRecords = 3`**
- 适用于灾难恢复,遇到 corruption 跳过不停止,继续回放后面的 records,尽可能多地恢复数据
## WriteLevel0TableForRecovery()
- 回放 WAL 时当 memtable 满了,进行 flush。
- 参数:
- **`cfd`**: `ColumnFamilyData`
- **`mem`**:`MemTable *`,flush 的目标 memtable
- **`edit`**:`VersionEdit *`,保存 flush 结果
- 将输出的 L0 文件和 Blob 文件加入其中
- 流程类似 [[flush#^c27f6d|WriteLevel0Table]],但只 flush 一个 memtable。
## GetLogSizeAndMaybeTruncate()
- 截断 wal preallocated 的部分,即保留到实际的文件大小。
- 作用:释放空间
- PR:[Always truncate the latest WAL file on DB Open #8122](https://github.com/facebook/rocksdb/pull/8122)
## RestoreAliveLogFiles()
- 恢复 `alive_log_files_` 和 `total_log_size_`
- 调用前提:
- WAL recovery 完后 memtable 中有数据,即 wal 中有 unflushed data,需要放入 `alive_logs_files`,避免被回收
- 否则就表示 wal 数据都 flush 过了,就可以视作 obsolete 的了
- 流程:
- 将大于等于 `versions_->MinLogNumberWithUnflushedData()` 的 wals 进行 GetLogSizeAndMaybeTruncate()后,放入 `alive_logs_files` 中,并累计 `total_log_size_`
## RecoverLogFiles()
- 回放 WAL 文件到 memtable。
- 入参:
- **`wal_number`**:`vector<uint64_t>`,wal 编号,从小到大
- 小于 `min_wal_number` 的 wals 不需要回放(已经 flush 到 SSTs)
- `min_wal_number`取以下两者较小值:
- `versions_->min_log_number_to_keep_`
- 来自 MANIFEST 回放
- `versions_->MinLogNumberWithUnflushedData())`
- 各个 cf 的 `log_number` 的最小值(即包含 unflushed 数据的最早WAL)
- 这是所有 cf 都适用的。每个 cf 根据自己的 `log_number`会作进一步筛选。
- `MemtableInserter::SeekToColumnFamily()`中
- wal record 回放:
1. 将 record 还原成 WriteBatch
2. 调用 `WriteBatchInternal::InsertInto()` 将 batch 插入到各个 cf 的 memtable 中
- 如果 wal 小于 cf 的 log_number,则会跳过 insert,避免数据重复(已 flush 过)
3. 检查 `flush_scheduler_`,如果有 cf 的 memtable 满了,则调用 WriteLevel0TableForRecovery() 进行 flush
- **整体流程**:
1. 依次读取每个 wal 的 records,进行回放,写入 memtable,满了则同步 flush
2. ==Final flush==:
- 检查每个 cf 的 memtable,如果不为空则调用 WriteLevel0TableForRecovery() 进行 flush
- 前提:之前 flush 过 或者 `avoid_flush_during_recovery` 为 false
- 默认情况下 `avoid_flush_during_recovery` 为 true,这样 recovery 完 memtable 都是空的了,都 flush 过了,之前的 wals 也不再需要了。
- **可能会产生小文件。**
3. 将 flush 输出的 VersionEdit 提交给 recovery context
4. wal 文件收尾处理
- 如果 wals 有 unflushed 的数据,则调用 RestoreAliveLogFiles(),放入 alive logs 中
- 否则只针对最后一个 wal(可能上次没写满),truncate 掉其 preallocated 部分
- `kPointInTimeRecovery` 的特殊逻辑:
- 中间有 corruption,但后面读到的 record 跟上一个 seq 是连续的,则忽略 corruption
- 如果 `cfd->GetLogNumber() > corrupted_wal_number && cfd->GetLiveSstFileSize() > 0` ,即 cf SST 非空且比损坏点新,则意味着两个 cf 之间数据可能不连续,不符合 PIT,返回失败
- 场景:同一个被损坏的 WAL 的数据,有的 cf flush 过了能恢复,有的 cf 无法恢复
> [!warning] paranoid_checks == false
> 如果 `DBOptions::paranoid_checks` 为 false,当读取某个 wal 出错时会直接忽略,继续回放
> - 见 MaybeIgnoreError()
> [!summary] RecoverLogFiles()
> - 主要是读取每个 wal 的 records,写入 memtable 进行数据回放;
> - 回放过程中 memtable 满了进行 flush
> - 各个 cf 根据`log_number`跳过本 cf 已经 flush 到 SST 的 wals,避免数据重复;
> - 回放期间 flush 产生的 L0 和 blob 文件,暂存到 RecoveryContext,后面再提交
---
# DBImpl
## NewDB()
- 初始化一个空的新 DB:
1. 创建 initial VersionEdit
- `log_number_` 为 0
- `next_file_number_`为 2(MANIFEST 占用了一个)
- `last_sequence_` 为 0
2. 创建 MANIFEST 文件 `MANIFEST-000001`,并写入 initial VersionEdit
3. 新建 CURRENT 文件指向 `MANIFEST-000001`
## MaybeUpdateNextFileNumber()
- 遍历 DB 目录下的所有文件
- 确定他们之中最大的文件序号,如果比 VersionSet 的 `next_file_number_` 的更大则更新
- 更新时同时生成一个 VesionEdit,放入 recovery context
- 收集所有 SST 和 Blob 文件,放入 recovery context
## Recover()
```mermaid
stateDiagram-v2
[*] --> LockFile
LockFile: 对 LOCK 加文件锁(如果不存在则创建)
LockFile --> CurrentExits
CurrentExits: CURRENT 文件是否存在
state CurrentChoice <<choice>>
CurrentChoice --> RecoverVersionSet: 存在
CurrentChoice --> CreateIfMissing: 不存在
CurrentExits --> CurrentChoice
CreateIfMissing: create_if_missing
state CreateIfMissingChoice <<choice>>
CreateIfMissingChoice --> NewDB: yes
CreateIfMissingChoice --> 返回出错: no
返回出错 --> [*]
CreateIfMissing --> CreateIfMissingChoice
NewDB: NewDB()
NewDB --> RecoverVersionSet
RecoverVersionSet: 调用 VersionSet Recover(), 回放 MAINIFEST,恢复 VersionSet 状态
RecoverVersionSet --> TriviallyMove
TriviallyMove: Trivially Move
TriviallyMove --> MaybeUpdateNextFileNumber
MaybeUpdateNextFileNumber: MaybeUpdateNextFileNumber()
note left of MaybeUpdateNextFileNumber
扫描目录下文件尝试更新 next_file_number,并收集 SST 和 Blob 文件
end note
MaybeUpdateNextFileNumber --> RecoverWAL
RecoverWAL: 回放 WAL:扫描 wal 目录,调用 RecoverLogFiles()
RecoverWAL --> [*]
```
- `error_if_exists == true`:如果存在 CURRENT 文件,则报错退出;
- [[version#^6ca17c|VersonSet Recover()]]
- **Trivially move**:
- PR: [Trivially move files down when opening db with level_compaction_dynamic_level_bytes #11321](https://github.com/facebook/rocksdb/pull/11321)
- 场景:从旧配置迁移到新配置 `level_compaction_dynamic_level_bytes = true`
- 例如:最大 7 层,将 lsm state 从 `[1, 3, 8, 0, 0, 0, 0]` 转换为 `[1, 0, 0, 0, 0, 3, 8]`
- 实现:将移动操作转换为 VerionEdits,放入 RecoveryContext 中
- 如果是 NewDB,且目录下存在 WAL 文件,则会报错
```
Open DB failed: Corruption: While creating a new Db, wal_dir contains existing log file: : 000200.log
```
## Open()
- 执行流程:
1. CreateDirIfMissing:
- 创建 wal、db 以及 archival 目录(如果有配置)
2. **DBImpl::Recover()**:
- 从 MANIFEST 加 wals 中恢复 db 状态;
3. CreateWAL():
- 创建一个新的 wal 文件
4. **LogAndApplyForRecovery()**:
- 提交 recovery 期间产生的 VersionEdits
- 会新建 MANIFEST
5. 创建 missing column families
- open 时指定的,但 recovery 后没发现的 cf
6. **InstallSuperVersionAndScheduleWork()**
- 为每个 cf 初始化 [[column family#^9e0dbe|sv]],并尝试调度 flush/compaction
- 至此各个 cf 就完整了,比如可以对外提供读能力
7. DeleteObsoleteFiles()
- full scan,扫描清理无效文件
8. StartPeriodicTaskScheduler()
- 定时任务,如 `stats_dump_period_sec`
> [!summary] Open
> - 主要是从 MANIFEST + wals 中恢复,以及初始化 cf 的 sv
---
# 总结
## Recovery context
- open 期间新 VersionEdit 的来源
- MaybeUpdateNextFileNumber()
- 更新 `next_file_number_`
- Triviall Move
- SST 文件跨 level 移动
- RecoverLogFiles()
- 回放 wal 时 flush memtble 产生新 L0 和 blob 文件
## SST preload
- Recover 时 SST 打开行为
- 如果 `max_open_files == -1`,则打开所有的 SST 文件;
- 否则,整个 DB 最多打开 16 个
- `max_open_files` 如果不为 -1 则还会受限于 `ulimit -n`
- `DBOptions::SanitizeOptions()`
---
# TODO
- [x] [Preload some files even if options.max_open_files #3340](https://github.com/facebook/rocksdb/pull/3340)
- [x] [Non-initial file preloading should always prefetch index and filter #4852](https://github.com/facebook/rocksdb/pull/4852)
- [x] open 时是否会新建 manifest (会)
- [x] `avoid_flush_during_recovery`
- [ ] `best_efforts_recovery`
- [Attempt to recover from db with missing table files #6334](https://github.com/facebook/rocksdb/pull/6334)
- [ ] `read_only`
---
# 实验
- [x] 验证 recover WAL 后,memtable 和 sst 中的数据是否可能有重复
- 不能重复,不然重复的 merge 操作造成错误的结果
- 如何保证:
- 每个 cf 切换 memtable 时也会切换新 wal
- memtable flush 后,通过 `VersionEdit::log_number_`同步保存新 wal number,避免回放老的 wal
- [x] 验证 Open 完 TableCache 的数量
- [ ] CURRENT丢失,有 SST files,如何处理