深入理解 RocksDB BottommostFiles compaction


First Published: 2026-03-28 | Last Revised: 2026-03-28

  1. 引言
  2. Bottommost File 判定规则
  3. Bottommost File 生成与标记
  4. Compaction 文件选择
  5. Compaction 输出层
  6. Compaction 收敛机制
  7. 总结

引言

本文基于 RocksDB v8.8.1 代码与文档,默认使用 leveled compaction 且 allow_ingest_behind=false,分析 BottommostFiles compaction 的工作原理、触发机制和收敛逻辑,并围绕以下问题展开:

  1. 哪些文件会出现在 bottommost_files_ 列表?Bottom 层的文件是不是就是 bottommost_files_?
  2. 如果读写都没有使用快照,BottommostFiles compaction 是否会触发?
  3. 每次释放快照的时候还是每次版本变更的时候会触发 BottommostFiles compaction?
  4. 每次后台 compaction 调度,BottommostFiles compaction 最多会选中多少个 bottommost_files_ 列表中的文件?
  5. BottommostFiles compaction 选择 bottommost_files_ 列表中的文件的时候的规则是怎样的?
  6. BottommostFiles compaction 的 input 和 output level 分别是多少?
  7. 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() 重新计算标记列表;SuggestCompactRangeCompactFilesImplBackgroundCompaction 失败重试等路径也会直接调用 ComputeCompactionScore()

生成后需经 ComputeBottommostFilesMarkedForCompaction() 标记才能进入 compaction 调度。标记需同时满足:!being_compactedlargest_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_snapshotbeing_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_ 中依次包含 FileAFileBFileCFileDFileE,并且每次都不触发 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 许可协议。转载请注明出处!

# 数据库