# 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 - ![](https://img.jonahgao.com/oss/note/2025p2/rocksdb_pointintime_recovery.svg) - 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,如何处理