引言
本文基于 RocksDB v8.8.1 代码与文档,默认使用 leveled compaction 且 allow_ingest_behind=false,分析 BottommostFiles compaction 的工作原理、触发机制和收敛逻辑,并围绕以下问题展开:
- 哪些文件会出现在 bottommost_files_ 列表?Bottom 层的文件是不是就是 bottommost_files_?
- 如果读写都没有使用快照,BottommostFiles compaction 是否会触发?
- 每次释放快照的时候还是每次版本变更的时候会触发 BottommostFiles compaction?
- 每次后台 compaction 调度,BottommostFiles compaction 最多会选中多少个
bottommost_files_列表中的文件? - BottommostFiles compaction 选择
bottommost_files_列表中的文件的时候的规则是怎样的? - BottommostFiles compaction 的 input 和 output level 分别是多少?
- BottommostFiles compaction 是怎么收敛的?
下面按代码调用链展开,正文会在流程中逐步回答以上问题。
Bottommost File 判定规则
先区分两个概念:Bottommost Files 不一定是 Bottom Level Files。Bottommost Files 的判断标准不是文件所在层级,而是:该文件的 key range 在所有更低层级中都没有重叠。BottommostFiles 的判定还有三个前置条件:
- L0 特例:仅
LevelFiles(0)中“当前顺序的最后一个文件”(即last_l0_idx == LevelFiles(0).size()-1)才可能被判定为 bottommost;其余 L0 文件在RangeMightExistAfterSortedRun()中会直接返回true。源码注释说明该行为用于保持历史语义,并提到可改为按 overlap 判断。 - allow_ingest_behind:启用该选项时,该机制会被跳过(不生成
bottommost_files_,也不会触发 BottommostFiles compaction)。 - 适用范围:该列表由
VersionStorageInfo生成并持有,LevelCompactionPicker在 leveled compaction 下消费它,其它 compaction style 不会使用。
// 在 GenerateBottommostFiles() 中的判断逻辑
void VersionStorageInfo::GenerateBottommostFiles() {
assert(!finalized_);
assert(bottommost_files_.empty());
for (size_t level = 0; level < level_files_brief_.size(); ++level) {
for (size_t file_idx = 0; file_idx < level_files_brief_[level].num_files; ++file_idx) {
const FdWithKeyRange& f = level_files_brief_[level].files[file_idx];
int l0_file_idx;
if (level == 0) {
l0_file_idx = static_cast<int>(file_idx);
} else {
l0_file_idx = -1;
}
Slice smallest_user_key = ExtractUserKey(f.smallest_key);
Slice largest_user_key = ExtractUserKey(f.largest_key);
// 关键判断:该文件的 key range 在更低层级是否存在
if (!RangeMightExistAfterSortedRun(smallest_user_key, largest_user_key,
static_cast<int>(level), l0_file_idx)) {
// 这是一个 bottommost 文件
bottommost_files_.emplace_back(static_cast<int>(level), f.file_metadata);
}
}
}
}
RangeMightExistAfterSortedRun() 的 L0 特例(仅当前顺序最后一个 L0 文件才可能继续判定):
if (last_level == 0 &&
last_l0_idx != static_cast<int>(LevelFiles(0).size() - 1)) {
return true;
}
例如,考虑以下 LSM 树结构:
L2: [FileA: a-m] [FileB: n-z]
L3: [FileC: a-f] [FileD: g-k] [FileE: x-z]
L4: [FileF: b-d]
在这种情况下:
- FileA (L2): 不是 bottommost(L3 的 FileC、FileD 与其重叠)
- FileB (L2): 不是 bottommost(L3 的 FileE 与其重叠)
- FileC (L3): 不是 bottommost(L4 的 FileF 与其重叠)
- FileD (L3): 是 bottommost(没有更低层级与 g-k 重叠)
- FileE (L3): 是 bottommost(没有更低层级与 x-z 重叠)
- FileF (L4): 是 bottommost(已经在最低层级)
Bottommost File 生成与标记
bottommost_files_ 在构建新 Version 时生成(PrepareForVersionAppend() → GenerateBottommostFiles()),释放快照不会重新生成该列表。每次安装新 Version(Flush、Compaction、SetOptions、IngestExternalFile、CreateColumnFamily 等,均通过 AppendVersion() 路径)都会触发 ComputeCompactionScore() → ComputeBottommostFilesMarkedForCompaction() 重新计算标记列表;SuggestCompactRange、CompactFilesImpl、BackgroundCompaction 失败重试等路径也会直接调用 ComputeCompactionScore()。
生成后需经 ComputeBottommostFilesMarkedForCompaction() 标记才能进入 compaction 调度。标记需同时满足:!being_compacted、largest_seqno != 0(尚未归零)、largest_seqno < oldest_snapshot_seqnum_,若设置了 bottommost_file_compaction_delay 还需满足时间条件。未满足序列号条件的文件会更新 bottommost_files_mark_threshold_(取所有未标记文件中 largest_seqno 的最小值),作为下次守卫判断的阈值。
void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(...) {
// ...
for (auto& level_and_file : bottommost_files_) {
if (!level_and_file.second->being_compacted &&
level_and_file.second->fd.largest_seqno != 0) {
if (level_and_file.second->fd.largest_seqno < oldest_snapshot_seqnum_) {
// 满足序列号条件,检查延迟后加入标记列表
bottommost_files_marked_for_compaction_.push_back(level_and_file);
} else {
// 未满足,更新阈值供下次守卫判断
bottommost_files_mark_threshold_ =
std::min(bottommost_files_mark_threshold_,
level_and_file.second->fd.largest_seqno);
}
}
}
}
标记条件中的关键变量 oldest_snapshot_seqnum_ 通过 UpdateOldestSnapshot() 推进,调用入口是 ReleaseSnapshot(),因此触发节奏与快照释放强相关。ReleaseSnapshot() 有两层守卫:DB 全局层面 oldest_snapshot > DBImpl::bottommost_files_mark_threshold_,CF 层面 oldest_snapshot_seqnum_ > VersionStorageInfo::bottommost_files_mark_threshold_,两层都通过才执行标记。
需要区分两种场景:如果应用从未创建过快照,oldest_snapshot_seqnum_ 始终为初始值 0,没有文件能满足 largest_seqno < 0,BottommostFiles compaction 不会被标记触发。但如果应用创建并释放了所有快照,ReleaseSnapshot 会将 oldest_snapshot 设为 GetLastPublishedSequence()(当前最新序列号),此时除 largest_seqno == oldest_snapshot、being_compacted == true 或未通过 delay 条件的文件外,其它满足条件的 Bottommost File 会被标记触发。
Compaction 文件选择
被标记的文件进入调度后,按以下规则选择压缩目标,流程以“先选一个起始文件”开始,但单次 compaction 可能包含多个文件:
选择起始输入后会调用 ExpandInputsToCleanCut(),在同一层级内扩展以确保 clean cut(注意:RoundRobin 扩展仅适用于 kLevelMaxLevelSize 类型的 compaction,不适用于 BottommostFiles compaction)。
假设有 5 个文件需要 BottommostFiles compaction,在不触发 clean-cut 扩展的简化情况下,(最多)需要 5 次调度才能完成所有文件的压缩。
BottommostFiles compaction 按 bottommost_files_marked_for_compaction_ 的顺序选择起始文件。具体实现在 LevelCompactionBuilder::PickFileToCompact() 中:
// 调用链:
// LevelCompactionBuilder::SetupInitialFiles()
// → 检查 score-based compaction
// → 检查 files_marked_for_compaction_
// → PickFileToCompact(BottommostFilesMarkedForCompaction(), false)
// ^^^^^
// compact_to_next_level = false
void LevelCompactionBuilder::PickFileToCompact(
const autovector<std::pair<int, FileMetaData*>>& level_files,
bool compact_to_next_level) {
for (auto& level_file : level_files) {
// 检查文件是否可以压缩
assert(!level_file.second->being_compacted);
start_level_ = level_file.first;
// 2. 检查层级限制,跳过不符合条件的层级
if ((compact_to_next_level &&
start_level_ == vstorage_->num_non_empty_levels() - 1) ||
(start_level_ == 0 &&
!compaction_picker_->level0_compactions_in_progress()->empty())) {
continue;
}
if (compact_to_next_level) {
output_level_ =
(start_level_ == 0) ? vstorage_->base_level() : start_level_ + 1;
} else {
output_level_ = start_level_; // 原地压缩
}
// 关键:只选择第一个符合条件的文件
start_level_inputs_.files = {level_file.second};
start_level_inputs_.level = start_level_;
// 尝试扩展输入文件以确保 clean cut,不破坏 SST 文件的原子边界
if (compaction_picker_->ExpandInputsToCleanCut(cf_name_, vstorage_, &start_level_inputs_)) {
return; // 成功选择文件,立即返回
}
}
// 如果没有找到合适的文件,清空输入
start_level_inputs_.files.clear();
}
假设 bottommost_files_marked_for_compaction_ 中依次包含 FileA、FileB、FileC、FileD、FileE,并且每次都不触发 clean-cut 扩展或 RoundRobin 扩展,那么 5 次调度会依次以这 5 个文件作为起始输入。
Compaction 输出层
选中文件后,compaction 的输出层级由 PickFileToCompact(..., false) 决定:compact_to_next_level = false 时执行 output_level_ = start_level_,因此 BottommostFiles compaction 为原地压缩(Input Level = Output Level)。原地压缩后文件仍输出到原层级,可消除的旧版本、墓碑或其它可裁剪内容在 compaction 中处理。
Compaction 收敛机制
BottommostFiles compaction 通过序列号归零实现收敛:compaction 输出的文件如果所有键的序列号都被归零,largest_seqno 变为 0,就不再满足 largest_seqno != 0 的标记条件,从而退出后续的 BottommostFiles compaction 循环。
收敛过程示例
假设当前有 4 个 Bottommost File,largest_seqno 分别不超过 100、200、300、400,而 oldest_snapshot_seqnum_ = 150。此时只有第一个文件满足标记条件,bottommost_files_mark_threshold_ 会收敛到其余未达标文件中的最小 largest_seqno,也就是 200。等 oldest_snapshot_seqnum_ 后续推进到 250 时,因为它已经越过 200,系统才会重新计算标记列表;这时第二个文件变为可标记对象。之后重复同样过程,直到文件被重写为 largest_seqno == 0,或者始终无法满足归零条件。这个例子对应的正是阈值跨越后才重新标记的节奏。
序列号归零的前提:Bottommost Level 判定
序列号归零不限于 BottommostFiles compaction——无论是什么类型的 Compaction,只要输出范围被判定为 Bottommost Level,满足条件的点键序列号都可能被归零。Bottommost Level 的判定基于本次 compaction 的输入范围(包括 clean-cut 扩展后的文件范围),而不是单个文件:
bool Compaction::IsBottommostLevel(
int output_level, VersionStorageInfo* vstorage,
const std::vector<CompactionInputFiles>& inputs) {
int output_l0_idx;
if (output_level == 0) {
output_l0_idx = 0;
for (const auto* file : vstorage->LevelFiles(0)) {
if (inputs[0].files.back() == file) {
break;
}
++output_l0_idx;
}
assert(static_cast<size_t>(output_l0_idx) < vstorage->LevelFiles(0).size());
} else {
output_l0_idx = -1;
}
Slice smallest_key, largest_key;
GetBoundaryKeys(vstorage, inputs, &smallest_key, &largest_key);
return !vstorage->RangeMightExistAfterSortedRun(smallest_key, largest_key,
output_level, output_l0_idx);
}
因此,当 ExpandInputsToCleanCut 扩展包含与下层有重叠的文件时,原本的 Bottommost File 可能不会在 Bottommost Level 上进行压缩,序列号归零也就不会发生。
序列号归零的 8 个条件
在 Bottommost Level 压缩过程中,CompactionIterator::PrepareOutput() 需要同时满足以下 8 个条件才会将序列号设置为 0:
void CompactionIterator::PrepareOutput() {
if (Valid()) {
//...
if (Valid() && compaction_ != nullptr &&
!compaction_->allow_ingest_behind() && // 条件1:不允许 ingest behind
bottommost_level_ && // 条件2:必须是 bottommost 层级
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) && // 条件3:序列号小于最早快照
ikey_.type != kTypeMerge && // 条件4:不是 Merge 操作
current_key_committed_ && // 条件5:键已提交
!output_to_penultimate_level_ && // 条件6:不输出到倒数第二层
ikey_.sequence < preserve_time_min_seqno_ && // 条件7:序列号小于时间保留阈值
!is_range_del_) { // 条件8:不是范围删除
ikey_.sequence = 0;
last_key_seq_zeroed_ = true;
//...
}
}
}
归零后,CompactionOutputs::AddToOutput() 使用归零后的序列号更新输出文件的 FileMetaData.largest_seqno。如果所有键都被归零,largest_seqno 最终为 0,该文件就不再被标记为待压缩。
特别注意
在标准的 BottommostFiles compaction 且输出范围最终仍被判定为 Bottommost Level 的场景下,条件 2(bottommost_level_)、条件 3(DefinitelyInSnapshot)、条件 5(current_key_committed_)和条件 8(!is_range_del_)由对应路径状态直接判定;条件 4(ikey_.type != kTypeMerge)是否满足取决于 merge operator 与该键历史,不能假设一定会被折叠为 kTypeValue。条件 6(!output_to_penultimate_level_)与条件 7(ikey_.sequence < preserve_time_min_seqno_)不满足时会直接阻断归零。
总结
BottommostFiles compaction 是 RocksDB 针对 Bottommost File 的一类定向重写机制。它在 largest_seqno < oldest_snapshot_seqnum_ 时才会被标记;如果该次 compaction 的输出范围又被判定为 Bottommost Level,满足条件的点键还可能执行序列号归零。也就是说,BottommostFiles 的“被调度”与 Bottommost Level 的“可归零”是两层不同的判断,它们相关,但并不等价。
本文作者 : cyningsun
本文地址 : https://www.cyningsun.com/03-28-2026/rocksdb-bottommost-files-compaction.html
版权声明 :本博客所有文章除特别声明外,均采用 CC BY-NC-ND 3.0 CN 许可协议。转载请注明出处!