LevelDB Compaction

Compaction的作用

leveldb是典型的LSM树实现,因此需要对内存中的数据进行持久化。一次内存数据的持久化过程,在leveldb中称为Minor Compaction。一次minor compaction的产出是一个0层的sstable文件,其中包含了所有的内存数据。但是若干个0层文件中是可能存在user_key overlap的。

正如前面的文章提到,leveldb是一个写效率十分高的存储引擎,存储的过程非常简单,只需要一次顺序的文件写和一个时间复杂度为O(log n)的内存操作即可。 相比来说,leveldb的读操作就复杂不少。首先需要进行一个复杂度为O(log n)的查询操作,若没有在内存中命中数据,则需要在按照数据的新旧程度在0层文件中依次进行查找遍历。由于0层文件中可能存在overlap,因此在最差情况下,可能需要遍历所有的文件。随着运行时间的增长,0层的文件个数会越来越多,在最差的情况下,查询一个数据需要遍历所有的数据文件,这显然是不可接受的。因此leveldb设计了一个Major Compaction的过程,将0层中的文件合并为若干个不存在user_key overlap的1层文件。对于不存在user_key overlap的文件,一次查找过程就可以进行优化,最多只需要一个文件的遍历即可完成。 除了level 0以外,任何一个level的文件内部是有序的,文件之间也是有序的。但是level(n)和level (n+1)中的几个文件的key可能存在交叉。正是因为这种交叉,查找某个key值的时候, level(n) 的查找无功而返,而不得不resort to level(n+1)。我们考虑寻找某一个key,如果找了曾经查找了level (n) ,但是没找到,然后去level (n+1)查找,结果找到了,那么对level (n)的某个文件而言,该文件就意味着有一次未命中。我们可以很容易想到,如果查找了多次,某个文件不得不查找,却总也找不到,总是去高一级的level,才能找到。这说明该层级的文件和上一级的文件,key的范围重叠的很严重,这是不合理的,会导致效率的下降。因此,需要对该level 发起一次Major compaction,减少 level 和level + 1的重叠情况。 因此,leveldb设计compaction的目的之一就是为了提高读取的效率。

0层文件个数规定

由于compaction的其中一个目的是为了提高读取的效率,因此leveldb不允许0层存在过多的文件数,(因为level 0的sstable文件之间存在overlap,所以读的时候要全部遍历)一旦超过了上限值,即可进行major compaction。

非0层文件数据大小限制

对于level i(i >= 1)的情况来说,一个读取最多只会访问一个sstable文件,因此,本身对于读取效率的影响不会太大。针对于这部分数据发生compaction的条件,从提升读取效率转变成了降低compaction的IO开销。故leveldb规定,1层文件总大小上限为10MB,2层为100MB,依次类推,最高层(7层)没有限制。

leveldb的每一条数据项都有一个版本信息,标识着这条数据的新旧程度。这也就意味着同样一个key,在leveldb中可能存在着多条数据项,且每个数据项包含了不同版本的内容。为了尽量减少数据集所占用的磁盘空间大小,leveldb在major compaction的过程中,对不同版本的数据项进行合并。注意snapshot对compaction的影响,见:snapshot 对 compaction 的影响

入口

无论是哪一种Compaction,入口点都是:DBImpl::MaybeScheduleCompaction() 其作用是检查是否需要进行Compaction,如果需要,那么就schedule一个DBImpl::BGWork给compaction线程。

void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  if (background_compaction_scheduled_) {
    // Already scheduled
    // 不需要再次schedule了,因为上次schedule的work完成了之后会再调MaybeScheduleCompaction()
    // 这时候会发现还需要compact,就会再schedule一个work
  } else if (shutting_down_.load(std::memory_order_acquire)) {
    // DB is being deleted; no more background compactions
  } else if (!bg_error_.ok()) {
    // Already got an error; no more changes
  } else if (imm_ == nullptr && manual_compaction_ == nullptr &&
             !versions_->NeedsCompaction()) {
    // 不需要minor compaction且不需要人工compaction且不需要major compaction
    // No work to be done
  } else {
    background_compaction_scheduled_ = true;
    env_->Schedule(&DBImpl::BGWork, this);
  }
}

BackgroundCompaction()是真正实施minor compaction或major compaction的地方

void DBImpl::BGWork(void* db) { // compaction线程来执行这个函数
  reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}

void DBImpl::BackgroundCall() { 
  MutexLock l(&mutex_); // 先拿锁
  assert(background_compaction_scheduled_);
  if (shutting_down_.load(std::memory_order_acquire)) {
    // No more background work when shutting down.
  } else if (!bg_error_.ok()) {
    // No more background work after a background error.
  } else {
    BackgroundCompaction();
  }

  background_compaction_scheduled_ = false;

  // Previous compaction may have produced too many files in a level,
  // so reschedule another compaction if needed.
  // 第一轮的Compaction可能会产生出很多files,
  // 可能需要再次发起一轮Compaction,需不需要就靠versions_->NeedsCompaction函数来判断。
  MaybeScheduleCompaction();
  background_work_finished_signal_.SignalAll();
}

DBImpl::~DBImpl() {
  // Wait for background work to finish.
  mutex_.Lock();
  shutting_down_.store(true, std::memory_order_release);
  while (background_compaction_scheduled_) {
    background_work_finished_signal_.Wait();
  }
  mutex_.Unlock();

  ...
}

https://img-blog.csdnimg.cn/20210601235922441.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center

minor compaction

一次minor compaction非常简单,其本质就是将一个内存数据库中的所有数据持久化到一个磁盘文件中。 https://img-blog.csdnimg.cn/54006ecf33c24c0e9357c6fec74a1d81.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAQnJhbnppbm8=,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center

在调用Put/Delete/Write API时,会首先检查DBImpl::MakeRoomForWrite (MakeRoomForWrite是在主线程被调用,而非后台 compaction 线程,具体在哪里讲读写流程的时候再展开)

// REQUIRES: mutex_ is held
// REQUIRES: this thread is currently at the front of the writer queue
Status DBImpl::MakeRoomForWrite(bool force) {
  mutex_.AssertHeld();
  assert(!writers_.empty());
  bool allow_delay = !force;
  Status s;
  while (true) {
    if (!bg_error_.ok()) {
      // Yield previous error
      s = bg_error_;
      break;
    } else if (allow_delay && versions_->NumLevelFiles(0) >=
                                  config::kL0_SlowdownWritesTrigger) {
      // this delay hands over some CPU to the compaction thread in
      // case it is sharing the same core as the writer thread.
      mutex_.Unlock();
      env_->SleepForMicroseconds(1000);
      allow_delay = false;  // Do not delay a single write more than once
      mutex_.Lock();
    } else if (!force &&
               (mem_->ApproximateMemoryUsage() <= options_.write_buffer_size)) {
      // There is room in current memtable
      break;
    } else if (imm_ != nullptr) {
      // We have filled up the current memtable, but the previous
      // one is still being compacted, so we wait.
      Log(options_.info_log, "Current memtable full; waiting...\n");
      background_work_finished_signal_.Wait();
    } else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
      // There are too many level-0 files.
      Log(options_.info_log, "Too many L0 files; waiting...\n");
      background_work_finished_signal_.Wait();
    } else {
      // Attempt to switch to a new memtable and trigger compaction of old
      assert(versions_->PrevLogNumber() == 0);
      uint64_t new_log_number = versions_->NewFileNumber();
      WritableFile* lfile = nullptr;
      s = env_->NewWritableFile(LogFileName(dbname_, new_log_number), &lfile);
      if (!s.ok()) {
        ...
      }
      delete log_;
      delete logfile_;
      logfile_ = lfile;
      logfile_number_ = new_log_number;
      log_ = new log::Writer(lfile);
      imm_ = mem_;
      has_imm_.store(true, std::memory_order_release);
      mem_ = new MemTable(internal_comparator_);
      mem_->Ref();
      force = false;  // Do not force another compaction if have room
      MaybeScheduleCompaction();
    }
  }
  return s;
}

但是当用户写入的速度始终大于major compaction的速度时,就会导致0层的文件数量还是不断上升,用户的读取效率持续下降。所以leveldb中规定(while循环检测):

  1. if 当0层文件数量超过SlowdownTrigger时,写入的速度减慢(sleep一会),再重新检测;

  2. else if 非强制(!force)且mem_->ApproximateMemoryUsage() < options_.write_buffer_size时,可以跳出循环,直接去写mem_了

  3. else if 当imm_ != nullptr时,写入暂停,直至Minor Compaction完成,再重新检查;

  4. else if 当0层文件数量超过PauseTrigger时,写入暂停,直至Major Compaction完成,再重新检查;

  5. else 即当mem_->ApproximateMemoryUsage() > options_.write_buffer_size: 5.1 启用新的WAL 5.2 启用新的mem_,让imm_指向老的mem_ 5.3 force = false 5.4 MaybeScheduleCompaction() MaybeScheduleCompaction()中是不放锁的,也就是说5完了之后,重新检查会走进2(因为新换了个mem_),然后会先写mem_把这次write完成,然后放锁,后台compaction线程拿到锁之后才会开始minor compaction

BackGroundCompaction函数很长,是个很长的故事,我们不贪多,直讲Minor Compaction,即如何将Immutable Memtable dump成SSTable 文件:

void DBImpl::BackgroundCompaction() {
  mutex_.AssertHeld();

  if (imm_ != nullptr) {
    CompactMemTable();
    return;
  }
  ...
}

void DBImpl::CompactMemTable() {
  mutex_.AssertHeld();
  assert(imm_ != nullptr);

  // Save the contents of the memtable as a new Table
  VersionEdit edit; // 搞一个 edit 来记录新增加的 sst 文件
  Version* base = versions_->current();
  base->Ref();
  Status s = WriteLevel0Table(imm_, &edit, base);
  base->Unref();

  if (s.ok() && shutting_down_.load(std::memory_order_acquire)) {
    s = Status::IOError("Deleting DB during memtable compaction");
  }

  // Replace immutable memtable with the generated Table
  if (s.ok()) {
    edit.SetPrevLogNumber(0); // always 0
    edit.SetLogNumber(logfile_number_); // 是 DBImpl::logfile_number_,就是现在正在使用的 log
    s = versions_->LogAndApply(&edit, &mutex_);
  }

  if (s.ok()) {
    // Commit to the new state
    imm_->Unref();
    imm_ = nullptr;
    has_imm_.store(false, std::memory_order_release);
    RemoveObsoleteFiles();
  } else {
    RecordBackgroundError(s);
  }
}

https://img-blog.csdnimg.cn/20210519154125728.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_17,color_FFFFFF,t_70#pic_center

Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  mutex_.AssertHeld();
  ...
  FileMetaData meta;
  meta.number = versions_->NewFileNumber();
  pending_outputs_.insert(meta.number);
  Iterator* iter = mem->NewIterator(); // 搞一个 Iterator
  Log(options_.info_log, "Level-0 table #%llu: started",
      (unsigned long long)meta.number);

  Status s;
  {
    mutex_.Unlock();
    // 构造sst然后写到盘里,这期间会放锁
    s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
    mutex_.Lock();
  }

  Log(options_.info_log, "Level-0 table #%llu: %lld bytes %s",
      (unsigned long long)meta.number, (unsigned long long)meta.file_size,
      s.ToString().c_str());
  delete iter;
  pending_outputs_.erase(meta.number);

  // Note that if file_size is zero, the file has been deleted and
  // should not be added to the manifest.
  int level = 0;
  if (s.ok() && meta.file_size > 0) {
    const Slice min_user_key = meta.smallest.user_key();
    const Slice max_user_key = meta.largest.user_key();
    if (base != nullptr) {
      level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
    }
    edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
                  meta.largest);
  }

  ...
  return s;
}

WriteLevel0Table

  1. BuildTable
  2. PickLevelForMemTableOutput
  3. edit->AddFile
int Version::PickLevelForMemTableOutput(const Slice& smallest_user_key,
                                        const Slice& largest_user_key) {
  int level = 0;
  /*如果和Level 0中的SSTable文件 user_key range 有重叠,直接返回 level = 0 */
  if (!OverlapInLevel(0, &smallest_user_key, &largest_user_key)) {
    // Push to next level if there is no overlap in next level,
    // and the #bytes overlapping in the level after that are limited.
    InternalKey start(smallest_user_key, kMaxSequenceNumber, kValueTypeForSeek);
    InternalKey limit(largest_user_key, 0, static_cast<ValueType>(0));
    std::vector<FileMetaData*> overlaps;
    /*while 循环寻找合适的level层级,最大level为2,不能更大*/
    while (level < config::kMaxMemCompactLevel) {
      if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {
        break;
      }
      if (level + 2 < config::kNumLevels) {
        // Check that file does not overlap too many grandparent bytes.
        GetOverlappingInputs(level + 2, &start, &limit, &overlaps);
        const int64_t sum = TotalFileSize(overlaps);
        if (sum > MaxGrandParentOverlapBytes(vset_->options_)) {
          break;
        }
      }
      level++;
    }
  }
  return level;
}

https://img-blog.csdnimg.cn/2021051917111194.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center

调用栈:

DBImpl::CompactMemTable()
---->VersionSet::LogAndApply(VersionEdit* edit, port::Mutex* mu)
--------->Version* v = new Version(this)
--------->builder.Apply(edit)
--------->builder.SaveTo(v)
--------->Finalize(v)
--------->AppendVersion(v)

我们先来看VersionSet::NeedsCompaction函数:(在 DBImpl::MaybeScheduleCompaction() 中被调用,用来检查是否需要做 major compaction)

bool VersionSet::NeedsCompaction() const {
    Version* v = current_;
    return (v->compaction_score_ >= 1) || (v->file_to_compact_ != NULL);
}

minor compaction过程中因为新生成了sst,所以要更新一波统计信息,

如果level0层文件的个数太多, 或者level i(i >= 1)层的文件总大小太大,超过门限值,则设置v->compaction_score为这一层的层号。

我们看下在什么情况下会重新计算compaction_score_。在Finalize函数中,会遍历各个level的文件数目和该level所有文件的总大小,给各个level打个分,如果没有一个level的分数是大于等于1,表示任何一个层级都不需要Compaction,但是如果存在某个或者某几个层级的score大于等于1,需要选择分最高的那个level进行major compaction。

0层文件个数规定

由于compaction的其中一个目的是为了提高读取的效率,因此leveldb不允许0层存在过多的文件数,(因为level 0的sstable文件之间存在overlap,所以读的时候要全部遍历)一旦超过了上限值,即可进行major compaction。

非0层文件数据大小限制

对于level i(i >= 1)的情况来说,一个读取最多只会访问一个sstable文件,因此,本身对于读取效率的影响不会太大。针对于这部分数据发生compaction的条件,从提升读取效率转变成了降低compaction的IO开销。故leveldb规定,1层文件总大小上限为10MB,2层为100MB,依次类推,最高层(7层)没有限制。

level 1               10M 
level 2              100M
level 3             1000M
level 4            10000M
level 5           100000M
level 6          1000000M
void VersionSet::Finalize(Version* v) {
  // Precomputed best level for next compaction
  int best_level = -1;
  double best_score = -1;

  for (int level = 0; level < config::kNumLevels - 1; level++) {
    double score;
    if (level == 0) {
      score = v->files_[level].size() /
              static_cast<double>(config::kL0_CompactionTrigger);
    } else {
      // Compute the ratio of current size to size limit.
      const uint64_t level_bytes = TotalFileSize(v->files_[level]);
      score =
          static_cast<double>(level_bytes) / MaxBytesForLevel(options_, level);
    }

    if (score > best_score) {
      best_level = level;
      best_score = score;
    }
  }

  v->compaction_level_ = best_level;
  v->compaction_score_ = best_score;
}

每一次compaction(不管是minor compaction或是major compaction)都会生成一个VersionEdit来记录新增或删除的sst文件信息,因此都需要VersionSet::LogAndApply,在这里面就会调用VersionSet::Finalize,里面就会更新v->compaction_score_ BackgroundCompaction()完成之后又会调一遍MaybeScheduleCompaction(),这里面就会调NeedsCompaction()来看是不是需要接着进行major compaction

major compaction

0层中浅蓝色的三个sstable文件,加上1层中的绿色的sstable文件,四个文件进行了合并,输出成两个按序组织的新的1层sstable文件进行替换。 https://img-blog.csdnimg.cn/99d665d6bfe04896976cee52f493efb3.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBAQnJhbnppbm8=,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center

尽管level > 0的sst的user_key range不存在overlap,但是请注意看上图,level1 的第二个sst的largest_user_key和第三个sst的smallest_user_key相等,都是150。尽管user_key重合,但是internal key却是不同的。即前一个sst的末尾的internal_key的seq一定大于后一个sst的第一个internal key的seq(seq越大值越小)。在非0层可能会出现有末端w唯一一个user_key重合的情况,这时由于在做major compaction的时候会ShouldStopBefore或者output_file的size达到阈值导致output_file中间截断造成的,不影响正确性。

我们再来看VersionSet::NeedsCompaction函数:

bool VersionSet::NeedsCompaction() const {
    Version* v = current_;
    return (v->compaction_score_ >= 1) || (v->file_to_compact_ != NULL);
}

那么什么时候,会触发leveldb进行major compaction呢。总结地来说为以下三个条件:

  1. 当0层文件数超过预定的上限 config::kL0_CompactionTrigger(默认为4个);
  2. 当level i层文件的总大小超过(10 ^ i) MB;
  3. 当某个文件无效读取的次数过多;

如果1或2有任意一个条件满足,那么v->compaction_score_ >= 1 会成立,是在VersionSet::Finalize中更新的,已经在上面讲过了。

如果3条件满足,那么v->file_to_compact_ != NULL会成立。 除了level 0以外,任何一个level的文件内部是有序的,文件之间也是有序的。但是level(n)和level (n+1)中的几个文件的key可能存在交叉。正是因为这种交叉,查找某个key值的时候, level(n) 的查找无功而返,而不得不resort to level(n+1)。我们考虑寻找某一个key,如果找了曾经查找了level (n) ,但是没找到,然后去level (n+1)查找,结果找到了,那么对level (n)的某个文件而言,该文件就意味着有一次未命中。 我们可以很容易想到,如果查找了多次,某个文件不得不查找,却总也找不到,总是去高一级的level,才能找到。这说明该层级的文件和上一级的文件,key的范围重叠的很严重,这是不合理的,会导致效率的下降。因此,需要对该level 发起一次Major compaction,减少 level 和level + 1的重叠情况。 这就是所谓的 seek compaction。

  1. Leveldb的作者认为,一个sst一次seek的开销为10ms, 若seek某一个sst文件但是却没有命中(没有找到user_key), 那么这就是一次无效seek,一段时间后,当某一sst文件的累计的无效seek的开销大于等于对该sst文件做major compaction开销的时候,这个sst文件就应该被compact了。

  2. 对于一个1MB的文件,对其合并的开销为250ms。(对于一个1MB的文件,其合并开销为 1. source层1MB的文件读取,2. source+1层 10-12MB的文件读取,3. source+1层10-12MB的文件写入。总结25MB的文件IO开销,除以100MB/s的文件IO速度,估计开销为250ms)因此当一个文件1MB的文件无效查询超过25次时,便可以对其进行合并。 每个sst文件的大小不一样,所以其允许的无效查询次数也不一样,sst的元数据,FileMetaData::allowed_seeks记录了这一数值,并且在VersionSet::Builder::Apply中初始化

// Apply all of the edits in *edit to the current state.
void VersionSet::Builder::Apply(VersionEdit* edit) {
  // Update compaction pointers
    for (size_t i = 0; i < edit->compact_pointers_.size(); i++) {
      const int level = edit->compact_pointers_[i].first;
      vset_->compact_pointer_[level] =
          edit->compact_pointers_[i].second.Encode().ToString();
    }
    
  // Add new files
  for (size_t i = 0; i < edit->new_files_.size(); i++) {
    const int level = edit->new_files_[i].first;
    FileMetaData* f = new FileMetaData(edit->new_files_[i].second);
    f->refs = 1;

    // We arrange to automatically compact this file after
    // a certain number of seeks.  Let's assume:
    //   (1) One seek costs 10ms
    //   (2) Writing or reading 1MB costs 10ms (100MB/s)
    //   (3) A compaction of 1MB does 25MB of IO:
    //         1MB read from this level
    //         10-12MB read from next level (boundaries may be misaligned)
    //         10-12MB written to next level
    // This implies that 25 seeks cost the same as the compaction
    // of 1MB of data.  I.e., one seek costs approximately the
    // same as the compaction of 40KB of data.  We are a little
    // conservative and allow approximately one seek for every 16KB
    // of data before triggering a compaction.
    f->allowed_seeks = static_cast<int>((f->file_size / 16384U));
    if (f->allowed_seeks < 100) f->allowed_seeks = 100;

    levels_[level].deleted_files.erase(f->number);
    levels_[level].added_files->insert(f);
  }
}

leveldb在正常的数据访问时,会顺带进行采样探测。正常的数据访问包括 (1)用户直接调用Get接口(2)用户使用迭代器进行访问。 记录本次访问的第一个sstable文件。 若在该文件中访问命中,则不做任何处理; 若在该文件中访问不命中,则对 该文件的allowed_seeks标志做减一操作(后面讲读写的时候讲细节)。 直到某一个文件的allowed_seeks标志减少到0时,触发对该文件的major compaction MaybeScheduleCompaction()

// Adds "stats" into the current state.  Returns true if a new
// compaction may need to be triggered, false otherwise.
// REQUIRES: lock is held
bool Version::UpdateStats(const GetStats& stats) {
  FileMetaData* f = stats.seek_file;
  if (f != nullptr) { // 查找的第一个 sst 文件,但是没找到目标 key
    f->allowed_seeks--;
    if (f->allowed_seeks <= 0 && file_to_compact_ == nullptr) { // 达到 seek compaction 的标准
    // 更新 Version::file_to_compact_,Version::file_to_compact_level_
    // 这样 NeedsCompaction() 就会返回 true
      file_to_compact_ = f;
      file_to_compact_level_ = stats.seek_file_level;
      return true;
    }
  }
  return false;
}

Status DBImpl::Get(const ReadOptions& options, const Slice& key,
                   std::string* value) {
  ...
  bool have_stat_update = false;
  Version::GetStats stats;
  
  // Unlock while reading from files and memtables
  {
    mutex_.Unlock();
    // First look in the memtable, then in the immutable memtable (if any).
    LookupKey lkey(key, snapshot);
    if (mem->Get(lkey, value, &s)) {
      // Done
    } else if (imm != nullptr && imm->Get(lkey, value, &s)) {
      // Done
    } else { // 从 mem 和 imm 中都没找到,接下来从 sst 中找,stats 将在这里面被赋值
      s = current->Get(options, lkey, value, &stats);
      have_stat_update = true;
    }
    mutex_.Lock();
  }

  if (have_stat_update && current->UpdateStats(stats)) {
  // 本次访问的第一个sstable文件 allow seek 次数为 0 了
    MaybeScheduleCompaction();
  }
  ...
}

对于非manual 而言,确定参战部队的番号,是由PickCompaction()来实现的。如果需要compaction的level是n,那么参加compaction的文件要么全部位于level n,要么既有level n的某些文件,也有level n+1的某些文件。 https://img-blog.csdnimg.cn/20210601170042708.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center 确定参与Compaction的参战文件,分成4步

  1. 第一步:确定level n的首个参战文件。
  2. 第二步:如果n==0,那么level n可能会有多个文件与首个参战文件有overlap,那么这时需要找出这些文件;如果n > 0则忽略这一步。 level 0 sst 之间是无序的,假设当前有 4 个文件,user_key range 分别是 [1, 8], [2, 6], [7, 8], [9, 10](按生成时间从旧到新排列)假如本次选择了第2个文件,如果只是把[2, 6]更新到 level 1,那么就会导致读取时数据错误。因为多个文件之间user_key是有重叠的,但每个key的seq是单调递增的。假如此时有一个 Get(5) 请求那么就会先从[1, 8]这个sst找5,假如说找到了5,那么说明这个sst里的5的seq <= Get请求的seq,这时将返回结果。然而[1, 8]这个sst里的seq都小于[2, 6]这个sst的seq,所以也有可能[2, 6]这个sst里的5的seq <= Get请求的seq,这个才是正确的结果(应该找到小于Get请求的seq中的最大的seq) 正确的做法就是当选出文件后,判断还有哪些文件有重叠,把这些文件都加入进来。
  3. 第三步:确定level n+1的参战文件,即找出和第一二步生成的文件有overlap的文件(因为要保证compaction完成之后,level n+1层的sst还是有序且相互之间没有overlap),当然也可能没有 level n+1的文件需要参战。
  4. 第四步:利用两层的输入文件,在不扩大level n+1输入文件的前提下,查找level n层的有overlap的文件,构成最终的输入文件;

https://img-blog.csdnimg.cn/20210601192659893.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center 如上图

  1. 红星标注的为起始输入文件;
  2. 在level i层中,查找与起始输入文件有key重叠的文件,如图中红线所标注,最终构成level i层的输入文件;
  3. 利用level i层的输入文件,在level i+1层找寻有key重叠的文件,结果为绿线标注的文件,构成level i,i+1层的输入文件;
  4. 最后利用两层的输入文件,在不扩大level i+1输入文件的前提下,查找level i层的有key重叠的文件,结果为蓝线标准的文件,构成最终的输入文件;

先看下class Compaction结构中用于major compaction的几个重要数据成员:

class Compaction 
{
...
	int level_;
	uint64_t max_output_file_size_;
	Version* input_version_;
	VersionEdit edit_; // 一个 compaction 对应一个 VersionEdit
	
	// Each compaction reads inputs from "level_" and "level_+1"
	//inputs_[0] 为level-n的sstable文件信息。
	//inputs_[1] 为level-n+1的sstable文件信息。
	std::vector<FileMetaData*> inputs_[2];  // The two sets of inputs
	
	// State used to check for number of overlapping grandparent files
	// (parent == level_ + 1, grandparent == level_ + 2)
	std::vector<FileMetaData*> grandparents_;
...
}
  1. input_[0]用于存放level层需要Compact的SSTable文件,input_[1]用于存放level+1层需要Compact的SSTable文件。
  2. grandparents_用于存放level+2层与Compact完的level+1层存在overlap的SSTable文件集合。

不同情况下发起的合并动作,其首个参战文件不同。

  1. 对于level 0层文件数过多引发的合并场景或由于level i层文件总量过大的合并场景(size_compaction),采用轮转的方法选择起始输入文件,current_->compaction_level_ 记录了需要进行size compaction的level,VersionSet::compact_pointer_记录了每层上一次size compaction合并的sst文件的最大key,下一次则选择在此key之后的首个文件。
class VersionSet {
  ...
  // Per-level key at which the next compaction at that level should start.
  // Either an empty string, or a valid InternalKey.
  std::string compact_pointer_[config::kNumLevels];
};
  1. 对于seek_compaction,起始输入文件则为该查询次数过多的文件,即current_->file_to_compact_
void DBImpl::BackgroundCompaction() {
  ... // 如果可以 minor compact,那么就先 minor
  
  Compaction* c;
  bool is_manual = (manual_compaction_ != nullptr);
  InternalKey manual_end;
  if (is_manual) {
    ...
  } else {
    c = versions_->PickCompaction();
  }
  ...
}

VersionSet::PickCompaction()会生成并初始化一个Compaction对象并返回

Compaction* VersionSet::PickCompaction() {
  Compaction* c;
  int level;

  // We prefer compactions triggered by too much data in a level over
  // the compactions triggered by seeks.
  // 若既满足size_compaction,又满足seek_compaction,优先选择size_compaction
  const bool size_compaction = (current_->compaction_score_ >= 1);
  const bool seek_compaction = (current_->file_to_compact_ != nullptr);
  if (size_compaction) {
    level = current_->compaction_level_;
    assert(level >= 0);
    assert(level + 1 < config::kNumLevels);
    c = new Compaction(options_, level);

    // Pick the first file that comes after compact_pointer_[level]
    // compact_pointer_[level] 记录了上一次该层合并的sst文件的最大key,
    // 下一次则选择在此key之后的首个文件。
    for (size_t i = 0; i < current_->files_[level].size(); i++) {
      FileMetaData* f = current_->files_[level][i];
      if (compact_pointer_[level].empty() ||
          icmp_.Compare(f->largest.Encode(), compact_pointer_[level]) > 0) {
        c->inputs_[0].push_back(f);
        break;
      }
    }
    if (c->inputs_[0].empty()) { // 上一次该层合并的sst文件的最大key已经是本层最大了
                                 // 这次就选最小的
      // Wrap-around to the beginning of the key space
      c->inputs_[0].push_back(current_->files_[level][0]);
    }
  } else if (seek_compaction) {
 // 对于seek_compaction,起始输入文件则为该查询次数过多的文件,即current_->file_to_compact_
    level = current_->file_to_compact_level_;
    c = new Compaction(options_, level);
    c->inputs_[0].push_back(current_->file_to_compact_);
  } else {
    return nullptr;
  }

  c->input_version_ = current_;
  c->input_version_->Ref();

只有当n为0时才会进行

  // Files in level 0 may overlap each other, so pick up all overlapping ones
  if (level == 0) {
    InternalKey smallest, largest;
    // GetRange stores the minimal range that covers all entries in inputs in *smallest, *largest.
    GetRange(c->inputs_[0], &smallest, &largest);
    // Note that the next call will discard the file we placed in
    // c->inputs_[0] earlier and replace it with an overlapping set
    // which will include the picked file.
    // 找出level 0中与首个参战文件有overlap的文件,append 到 c->inputs_[0]
    current_->GetOverlappingInputs(0, &smallest, &largest, &c->inputs_[0]);
    assert(!c->inputs_[0].empty());
  }
  //此时input[0]记录了level n`层需要Compact的文件,
  //交由SetupOtherInputs()去填充input[1]和更新input[0]

遍历inputs,找到最小的input->smallest,和最大的input->largest(InternalKey)

void VersionSet::GetRange(const std::vector<FileMetaData*>& inputs,
                          InternalKey* smallest, InternalKey* largest);

遍历inputs1和inputs2,找到最小的input->smallest,和最大的input->largest(InternalKey)

void VersionSet::GetRange2(const std::vector<FileMetaData*>& inputs1,
                           const std::vector<FileMetaData*>& inputs2,
                           InternalKey* smallest, InternalKey* largest);

该level中的所有sst,只要是其user_key range 和 [begin->user_key(), end->user_key()],就算是有overlap,会被append在inputs里

// Store in "*inputs" all files in "level" that overlap [begin,end]
void Version::GetOverlappingInputs(int level, const InternalKey* begin,
                                   const InternalKey* end,
                                   std::vector<FileMetaData*>* inputs)
  SetupOtherInputs(c);

  return c;
}
void VersionSet::SetupOtherInputs(Compaction* c) {
  const int level = c->level();
  InternalKey smallest, largest;

  AddBoundaryInputs(icmp_, current_->files_[level], &c->inputs_[0]);
  // 返回 level 层所有参战文件(c->inputs_[0])最大和最小的 internal key
  GetRange(c->inputs_[0], &smallest, &largest);

 // 找到 level + 1 层所有与 level 层所有参战文件有 overlap 的文件 append 到 c->inputs_[1]
  current_->GetOverlappingInputs(level + 1, &smallest, &largest,
                                 &c->inputs_[1]);

AddBoundaryInputs这个函数不展开讲了,是为了解决非0层sst会出现末端单个user_key重合的情况。可以看这个博客

  // Get entire range covered by compaction
  InternalKey all_start, all_limit;
  // 找出c->inputs_[0]和c->inputs_[1]中所有sst中最小和最大的internal key
  GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);

  // See if we can grow the number of inputs in "level" without
  // changing the number of "level+1" files we pick up.
  if (!c->inputs_[1].empty()) {
    std::vector<FileMetaData*> expanded0;
    // 在 level n 中构建扩展过后的参战文件集合 expanded0
    current_->GetOverlappingInputs(level, &all_start, &all_limit, &expanded0);
    AddBoundaryInputs(icmp_, current_->files_[level], &expanded0);
    const int64_t inputs0_size = TotalFileSize(c->inputs_[0]);
    const int64_t inputs1_size = TotalFileSize(c->inputs_[1]);
    const int64_t expanded0_size = TotalFileSize(expanded0);
    if (expanded0.size() > c->inputs_[0].size() && // expanded0 中参战的sst个数多于c->inputs_[0],说明level n参战的sst被扩展了
        inputs1_size + expanded0_size <
            ExpandedCompactionByteSizeLimit(options_)) {
      // 根据 expend0 来重新生成 level n+1 的参战文件集合 expanded1
      InternalKey new_start, new_limit;
      GetRange(expanded0, &new_start, &new_limit);
      std::vector<FileMetaData*> expanded1;
      current_->GetOverlappingInputs(level + 1, &new_start, &new_limit,
                                     &expanded1);
      if (expanded1.size() == c->inputs_[1].size()) { // 没有扩展 level n+1 的参战集合
        ...
        smallest = new_start;
        largest = new_limit;
        // reset c->inputs_[0] c->inputs_[1]
        c->inputs_[0] = expanded0;
        c->inputs_[1] = expanded1;
        GetRange2(c->inputs_[0], c->inputs_[1], &all_start, &all_limit);
      }
    }
  }
  // Compute the set of grandparent files that overlap this compaction
  // (parent == level+1; grandparent == level+2)
  if (level + 2 < config::kNumLevels) {
    current_->GetOverlappingInputs(level + 2, &all_start, &all_limit,
                                   &c->grandparents_);
  }

  // Update the place where we will do the next compaction for this level.
  // We update this immediately instead of waiting for the VersionEdit
  // to be applied so that if the compaction fails, we will try a different
  // key range next time.
  // 记录在这个 edit_ 中,会在 VersionSet::Builder::Apply 更新到 VersionSet 里
  compact_pointer_[level] = largest.Encode().ToString();
  c->edit_.SetCompactPointer(level, largest);
}
void DBImpl::BackgroundCompaction() {
  ...

  Compaction* c;
  bool is_manual = (manual_compaction_ != nullptr);
  InternalKey manual_end;
  if (is_manual) {
    ...
  } else {
    c = versions_->PickCompaction();
  }

  Status status;
  if (c == nullptr) {
    // Nothing to do
  } else if (!is_manual && c->IsTrivialMove()) {
    // Move file to next level
    assert(c->num_input_files(0) == 1);
    FileMetaData* f = c->input(0, 0);
    c->edit()->RemoveFile(c->level(), f->number);
    c->edit()->AddFile(c->level() + 1, f->number, f->file_size, f->smallest,
                       f->largest);
    status = versions_->LogAndApply(c->edit(), &mutex_);
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    ...

什么条件下,可以直接使用原文件,而节省重新生成文件这个过程? 那就是Compaction::IsTrivialMove(),会检查下面3个条件 同时满足以下条件时,我们只要简单的把文件从level标记到level + 1层就可以了

  1. level层只有一个文件
  2. level + 1层没有文件
  3. 跟level + 2层overlap的文件没有超过阈值(实际上是20M) 注:条件3主要是(避免mv到level + 1后,导致level + 1 与 level + 2层compact压力过大)

https://img-blog.csdnimg.cn/20210602170204582.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center

void DBImpl::BackgroundCompaction() {
  ...
  
  } else {
  // CompactionState 负责记录此次 compaction 的状态
    CompactionState* compact = new CompactionState(c); 
    status = DoCompactionWork(compact);
    if (!status.ok()) {
      RecordBackgroundError(status);
    }
    CleanupCompaction(compact);
    c->ReleaseInputs();
    RemoveObsoleteFiles();
  }
  delete c;

  if (status.ok()) {
    // Done
  } else if (shutting_down_.load(std::memory_order_acquire)) {
    // Ignore compaction errors found during shutting down
  } else {
    Log(options_.info_log, "Compaction error: %s", status.ToString().c_str());
  }

  if (is_manual) {
    ...
  }
}

因为snapshot语义上理解是对DB的快照,那么读snapshot的某个user_key,就应该读到的是做snapshot那一时刻前该user_key的状态,也就是说应该读到小于snapshot_seq的seq中最大的一个 seq,因为相同user_key,seq越小,internal_key越大。也就是应该读到大于 user_key+snapshot_seq 的 InternalKey 中最小的一个(FindGreaterOrEqual) 小于smallest_snapshot的Sequence Number是不重要的,因为我们不会为提供小于smallest_snapshot的snapshot。 https://img-blog.csdnimg.cn/20210602172515301.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center id=2不能drop,因为我们最多可以提供seq = 最旧的snapshot的snapshot,这个snapshot需要看得见id=2。 https://img-blog.csdnimg.cn/20210602172940601.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl80MjY1MzQwNg==,size_16,color_FFFFFF,t_70#pic_center 但是如果id=2是一个删除操作(墓碑),那么id=2可以drop,因为drop了就看不见了 = 删除了

Status DBImpl::DoCompactionWork(CompactionState* compact) {
  ...

  assert(versions_->NumLevelFiles(compact->compaction->level()) > 0);
  assert(compact->builder == nullptr);
  assert(compact->outfile == nullptr);
  if (snapshots_.empty()) { // 用户此时不持有 DB 的 snapshot,获取最后一条写入数据的 seq
    compact->smallest_snapshot = versions_->LastSequence(); 
  } else { // 获取最旧 snapshot 的 seq
    compact->smallest_snapshot = snapshots_.oldest()->sequence_number();
  }

  // 生成多路归并迭代器
  Iterator* input = versions_->MakeInputIterator(compact->compaction);

  // Release mutex while we're actually doing the compaction work
  mutex_.Unlock();

  input->SeekToFirst();
  Status status;
  ParsedInternalKey ikey;
  std::string current_user_key;
  bool has_current_user_key = false;
  SequenceNumber last_sequence_for_key = kMaxSequenceNumber;
  while (input->Valid() && !shutting_down_.load(std::memory_order_acquire)) {
    // Prioritize immutable compaction work
    // 如果发现需要进行minor compact,那么这个compact线程会暂停执行现在正在执行的major compact
    // 转去先执行minor compact
    if (has_imm_.load(std::memory_order_relaxed)) {
      mutex_.Lock();
      if (imm_ != nullptr) {
        CompactMemTable();
        // Wake up MakeRoomForWrite() if necessary.
        background_work_finished_signal_.SignalAll();
      }
      mutex_.Unlock();
    }

    Slice key = input->key(); // key 是一个 encode 过的 internal key
    // 如果目前 compact 生成的文件,
    // 会导致接下来 level+1 && level+2 层compact压力过大,
    // 那么FinishCompactionOutputFile(compact, input)会结束当前output文件的compact.
    //(overlapped_bytes_ > MaxGrandParentOverlapBytes(vset->options_))
	// 因此,每次都会调用ShouldStopBefore来判断是否满足上述条件
    if (compact->compaction->ShouldStopBefore(key) &&
        compact->builder != nullptr) {
      status = FinishCompactionOutputFile(compact, input);
      if (!status.ok()) {
        break;
      }
    }

    // Handle key/value, add to state, etc.
    bool drop = false;
    // decode key 为 ikey
    if (!ParseInternalKey(key, &ikey)) { // decode 失败
      ...
    } else {
      if (!has_current_user_key ||
          user_comparator()->Compare(ikey.user_key, Slice(current_user_key)) !=
              0) {
        // First occurrence of this user key
        // 相同user key可能会有多个,seq越大表示越新,顺序越靠前
        // 第一次碰到该user_key,标记has_current_user_key为true, sequence为max值
        // 第一次碰到该user_key,说明这个internalkey的seq是所有该user_key中最大的
        // 也就是该user_key的最后一次写入
        // 所以不能drop
        current_user_key.assign(ikey.user_key.data(), ikey.user_key.size());
        has_current_user_key = true;
        last_sequence_for_key = kMaxSequenceNumber;
      }

	  //由于多次Put/Delete,有些key会出现多次
      //有几种情况在compact时drop key:
      //1. 对于多次出现的user key,我们需要保留
      //     (1)所有 > compact->smallest_snapshot 的 seq
      //     (2)所有 <= compact->smallest_snapshot 的 seq 中最大的一个 seq
      //   当第一次碰到某user_key时设置last_sequence_for_key = kMaxSequenceNumber
      //   以及跟compact->smallest_snapshot比较,可以保证这两点
      //2. 如果是kTypeDeletion && seq <= snapshot && 更高层没有该key,那么也可以忽略
      if (last_sequence_for_key <= compact->smallest_snapshot) { 
        // rule (A)
        // Hidden by an newer entry for same user key
        drop = true;  
      } else if (ikey.type == kTypeDeletion &&
                 ikey.sequence <= compact->smallest_snapshot &&
                 compact->compaction->IsBaseLevelForKey(ikey.user_key)) {
        // For this user key:
        // (1) there is no data in higher levels
        // (2) data in lower levels will have larger sequence numbers
        // (3) data in layers that are being compacted here and have
        //     smaller sequence numbers will be dropped in the next
        //     few iterations of this loop (by rule (A) above).
        // Therefore this deletion marker is obsolete and can be dropped.
        drop = true;
      }

      last_sequence_for_key = ikey.sequence; // 更新 last_sequence_for_key
    }

    if (!drop) {
      // Open output file if necessary
      if (compact->builder == nullptr) {
        status = OpenCompactionOutputFile(compact);
        if (!status.ok()) {
          break;
        }
      }
      if (compact->builder->NumEntries() == 0) {
        compact->current_output()->smallest.DecodeFrom(key);
      }
      compact->current_output()->largest.DecodeFrom(key);
      // 写入这条 kv
      compact->builder->Add(key, input->value());

      // Close output file if it is big enough
      if (compact->builder->FileSize() >=
          compact->compaction->MaxOutputFileSize()) {
        status = FinishCompactionOutputFile(compact, input);
        if (!status.ok()) {
          break;
        }
      }
    }
    
   // 继续循环
    input->Next();
  }

  if (status.ok() && shutting_down_.load(std::memory_order_acquire)) {
    status = Status::IOError("Deleting DB during compaction");
  }
  if (status.ok() && compact->builder != nullptr) {
    status = FinishCompactionOutputFile(compact, input);
  }
  if (status.ok()) {
    status = input->status();
  }
  delete input;
  input = nullptr;
  
  mutex_.Lock();
  

  if (status.ok()) {
    status = InstallCompactionResults(compact);
  }
  if (!status.ok()) {
    RecordBackgroundError(status);
  }
  return status;
}

Status DBImpl::InstallCompactionResults(CompactionState* compact) {
  mutex_.AssertHeld();
  
  // Add compaction outputs
  // 将此次 compaction 的参战文件全部记录在 VersionEdit::deleted_files_
  compact->compaction->AddInputDeletions(compact->compaction->edit());
  const int level = compact->compaction->level();
  for (size_t i = 0; i < compact->outputs.size(); i++) {
  // 将此次 compaction 的生成文件全部记录在 VersionEdit::new_files_
    const CompactionState::Output& out = compact->outputs[i];
    compact->compaction->edit()->AddFile(level + 1, out.number, out.file_size,
                                         out.smallest, out.largest);
  }
  // apply 此次 compaction 的 version edit
  return versions_->LogAndApply(compact->compaction->edit(), &mutex_);
}

生成 compaction 过程中使用的迭代器

Iterator* VersionSet::MakeInputIterator(Compaction* c) {
  ReadOptions options;
  options.verify_checksums = options_->paranoid_checks;
  options.fill_cache = false;

  // Level-0 files have to be merged together.  For other levels,
  // we will make a concatenating iterator per level.
  const int space = (c->level() == 0 ? c->inputs_[0].size() + 1 : 2);
  Iterator** list = new Iterator*[space];
  int num = 0;
  for (int which = 0; which < 2; which++) {
    if (!c->inputs_[which].empty()) {
      if (c->level() + which == 0) {
        const std::vector<FileMetaData*>& files = c->inputs_[which];
        for (size_t i = 0; i < files.size(); i++) {
          list[num++] = table_cache_->NewIterator(options, files[i]->number,
                                                  files[i]->file_size);
        }
      } else {
        // Create concatenating iterator for the files from this level
        list[num++] = NewTwoLevelIterator(
            new Version::LevelFileNumIterator(icmp_, &c->inputs_[which]),
            &GetFileIterator, table_cache_, options);
      }
    }
  }
  assert(num <= space);
  Iterator* result = NewMergingIterator(&icmp_, list, num);
  delete[] list;
  return result;
}

参考

https://catkang.github.io/2017/02/03/leveldb-version.html https://leveldb-handbook.readthedocs.io/zh/latest/compaction.html https://bean-li.github.io/leveldb-compaction-3/ https://blog.csdn.net/H514434485/article/details/108565204 https://izualzhy.cn/leveldb-version-minor-use https://www.ravenxrz.ink/archives/1ba074b9.html http://www.calvinneo.com/2021/04/18/leveldb-compaction/