LevelDB源码:Flush的流程

本文将从源码方面分析 Flush 的流程

博客只能分析部分,完整分析见代码注释:LevelDB-annotated

一般而言,分析一个流程要从入口点还是逐层向下分析,但这样带来的后果是主次的详略不当。通常,一种操作的核心函数只有一两个,然后衍生该函数至一整个调用链就构成了操作流程,而该核心函数最直接的描述了思想,重中之重。

因此,本文没有采用从入口分析的方式,而是先分析核心函数 WriteLevel0Table(),然后完善其调用链。

WriteLevel0Table

该函数顾名思义,写 Level0 的 SST,那其实就是 Flush,Flush的真正执行正是该函数。实际上,WriteLevel0Table 并不一定是写 Level0,还有可能是写 Level1 或是 Level2,这取决于顶层的情况。

该函数的工作只有一个:把 imm_ 给 Flush 了。

分为以下三步执行:

  1. 通过 BuildTable() 来生成 SST
  2. 通过 PickLevelForMemTableOutput() 来选出存放该 SST 的 Level
  3. 通过 edit->AddFile() 将新的 SST 加入 VersionEdit 中

先看源码:

Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  mutex_.AssertHeld();
  const uint64_t start_micros = env_->NowMicros();
  FileMetaData meta;
  meta.number = versions_->NewFileNumber();
  // 加入待生成SST列表
  pending_outputs_.insert(meta.number);
  Iterator* iter = mem->NewIterator();
  Log(options_.info_log, "Level-0 table #%llu: started",
      (unsigned long long)meta.number);

  Status s;

  // 根据im的iter生成SST
  {
    mutex_.Unlock();
    // 注意,BuildTable是包含落盘的
    // SST中的一些信息(最大最小key、文件大小)等会记录在meta中
    s = BuildTable(dbname_, env_, options_, table_cache_, iter, &meta);
    // 至此,SST已经在磁盘中了
    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());
  // 释放iter
  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) {
      // 从当前Version中选出存放该SST的Level
      // 一般来说是0,当然也可能不是
      // LevelDb的思想:
      // 尽可能放在2层,因为level0层经常被访问,内部又允许重叠,如果文件过多,会放大读性能
      level = base->PickLevelForMemTableOutput(min_user_key, max_user_key);
    }
    // 新的SST加入new_files_中
    edit->AddFile(level, meta.number, meta.file_size, meta.smallest,
                  meta.largest);
  }

  CompactionStats stats;
  stats.micros = env_->NowMicros() - start_micros;
  stats.bytes_written = meta.file_size;
  stats_[level].Add(stats);
  return s;
}

逐步的分析这里就不写了,挺简单的,并且注释都写有。有几个需要注意的地方:

BuildTable() 不仅仅是构建 SST 在内存中的结构,而是直接将 SST 落盘,至于怎么构建又怎么落盘的,这需要深入 SST 部分的源码了,大致是通过 builder 来构建一个个 block,然后整合在一起进行刷盘,这里就不深究了,具体去看源码注释

只需明白:先生成 SST,在决定它放在哪一层。

生成 SST 之后,通过 PickLevelForMemTableOutput() 来决定它放在哪一层。问题来了,Flush 难道不就是放在第一层吗,为什么还要通过函数来决定?还真不是。在 LevelDb 的思想中,Flush 的文件尽可能放在2层,因为0层经常被访问,内部又允许重叠,如果文件过多,会放大读。

但是不是想往下放就能往下放的,要满足以下条件(假设 level 是最终目标层):

  • SST 不能和 level+1 有重叠
  • SST 可以和 level+2 有重叠,但重叠不能超过阈值

源码如下:

// 从当前Version中选出存放从memtable下刷的SST的Level
// 一般来说,是Level0
// 但是,如果Level和该SST没有重叠,那么就直接放在Level1更好
// 依此类推,如果没有重叠,还可以放到Level2
// 直到出现重叠或达到kMaxMemCompactLevel(2)
int Version::PickLevelForMemTableOutput(const Slice& smallest_user_key,
                                        const Slice& largest_user_key) {
  int 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 < config::kMaxMemCompactLevel) {
      // 不能和level+1有重叠
      if (OverlapInLevel(level + 1, &smallest_user_key, &largest_user_key)) {
        break;
      }
      // 可以和level+2有重叠,但重叠不能超过阈值
      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;
}

先生成 SST,在决定它放在哪一层,就引入了一个问题:LSM-Tree 中到底如何记录每一层有哪些 SST 呢?

=> 该问题在上一篇博客中回答了:LevelDB层级实现的思考

至此,WriteLevel0Table() 就介绍完毕了,Flush 的核心也就介绍完毕了。

接下来介绍调用链。

MakeRoomForWrite

该函数就是字面意思,为 Write 腾出空间,也就是看情况是否新建 memtable。不需要,则直接退出;需要,则新建 memtable。而 Flush 的入口,就在该函数中。

函数的流程如下:

  1. 如果允许 delay,而且 L0 的文件个数没有超过 kL0_SlowdownWritesTrigger,那么就等 1s 重来,但是只能等1次。
  2. 如果不允许 delay,且 memtable 有足够的空间,说明不需要新建了,直接退出。
  3. 在第 2 步为否的基础上(memtable 满了),如果 imm_ 不为空,则说明 memtable 满了且当前的 imm_ 还没有被 Flush 掉,此时已经没有空间分配出去了,因为 mem_ 和 imm_ 各只能有一个,那么就一直等待,直到Flush 完成后唤醒它,然后重新进入循环
  4. 在前几步为否的基础上(memtable满了,imm_ 为空),如果 L0 的文件个数超过 kL0_StopWritesTrigger,就说明 L0 文件太多了,那么就等待,直到 L0 被 Compaction 了之后在被唤醒。
  5. 在前几步为否的基础上(memtable满了,imm_ 为空,L0空间足够),将mem_ 赋值给 imm_ ,然后重建一个新的 mem_ ,即下刷旧的生成新的。随后,调用 MaybeScheduleCompaction() 来将imm_ 给 Flush 掉。

源码如下:

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) {
      // We are getting close to hitting a hard limit on the number of
      // L0 files.  Rather than delaying a single write by several
      // seconds when we hit the hard limit, start delaying each
      // individual write by 1ms to reduce latency variance.  Also,
      // this delay hands over some CPU to the compaction thread in
      // case it is sharing the same core as the writer.
      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.
      // mem_满了,且imm_仍存在,那么就等待直到imm_被Flush
      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 {
      // mem_满了,imm_为空
      // 那就把mem_赋值给imm_,然后重建一个新的mem_
      // 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()) {
        // Avoid chewing through file number space in a tight loop.
        versions_->ReuseFileNumber(new_log_number);
        break;
      }
      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
      // 将imm_给Flush
      MaybeScheduleCompaction();
    }
  }
  return s;
}

当 MakeRoomForWrite() 判断出 imm_ 存在且能够 Flush 后,会调用 MaybeScheduleCompaction() 更进一步。

MaybeScheduleCompaction

该函数的作用就是判断是否需要调度后台 Compaction 任务。注意,LevelDB 的后台任务均为 BGWork,不管是 Flush 还是 Compaction,并且后台线程只能有一个,这是其不同于 RocksDB 的一个主要特征。MaybeScheduleCompaction 仍在主线程中执行,但其结果会新开一个后台线程。代码如下:

// 可能要进行Compaction
void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  if (background_compaction_scheduled_) {
    // 压缩线程已经在执行了
    // Already scheduled
  } else if (shutting_down_.load(std::memory_order_acquire)) {
    // db正在被关闭
    // 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()) {
    // imm_为空,且LSM-Tree不需要Compaction
    // No work to be done
  } else {
    // 三种可能情况:
    // 1.imm_不为空,需要Flush
    // 2.manual_compaction_不为空,需要手动开启压缩
    // 3.LSM-Tree需要压缩

    // 调度Compaction
    background_compaction_scheduled_ = true;
    // Schedule()就是另起一个线程去执行传入函数
    env_->Schedule(&DBImpl::BGWork, this);
  }
}

通过 background_compaction_scheduled_ 可以看出,后台线程只能有一个。

以下三种情况,Compaction / Flush 会发生:

  • imm_不为空,需要Flush
  • manual_compaction_不为空,需要手动开启压缩
  • LSM-Tree需要压缩

其中第一种就是 FLush。

env_->Schedule(&DBImpl::BGWork, this); 就是另开一个线程执行 BGWork 函数,而BGWork 实际上是调用 BackgroundCall() 函数。

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 {
    // 执行Compaction
    // 因为此时已经在新线程了,所以BackgroundCompaction()是个串行执行
    BackgroundCompaction();
  }

  // Compaction完毕
  background_compaction_scheduled_ = false;

  // Previous compaction may have produced too many files in a level,
  // so reschedule another compaction if needed.
  // 重新判断是否需要Compaction
  // 因为前一个Compaction如果过大的话可能产生太多新的SST导致需要另一次Compaction
  MaybeScheduleCompaction();
  background_work_finished_signal_.SignalAll();
}

可以看出,它就是调用 BackgroundCompaction() 函数,执行完后重新执行一遍 MaybeScheduleCompaction(),因为前一个 Compaction 如果过大的话可能产生太多新的 SST 导致需要另一次Compaction。

BackgroundCompaction

该函数我们只看前面一点:

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

  // imm_不为空的话,则执行Flush
  // 执行完后返回即可
  if (imm_ != nullptr) {
    CompactMemTable();
    return;
  }
  
  // ... 省略很多代码
}

可以看到,如果 imm_ 不为空,那么就执行 CompactMemTable(),也就是 Flush,执行完之后返回即可。

CompactMemTable

直接看代码:

// 把im memtable给flush到L0中
void DBImpl::CompactMemTable() {
  mutex_.AssertHeld();
  assert(imm_ != nullptr);

  // Save the contents of the memtable as a new Table
  // 新建VerionEdit对应本次Flush
  VersionEdit edit;
  // 当前所在的Version
  Version* base = versions_->current();
  // Version加引用
  base->Ref();
  // 执行Flush
  Status s = WriteLevel0Table(imm_, &edit, base);
  // Version解引用
  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);
    edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
    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);
  }
}

这就连上了,CompactMemTable() 的工作就是在进行一些 Version 操作之后,调用 WriteLevel0Table() 。

总结

综上,解答如下问题:

  1. WriteLevel0Table() 何时调用?
  2. 由谁调用?
  3. 单线程串行调用还是多线程并行调用?

答:

  1. imm_ 满时调用。
  2. 最直接的调用者是CompactMemTable()。继续往前回溯,由 MakeRoomForWrite() 逐步调用。
  3. 单线程,LevelDB 的后台线程只有一个,不管是 Flush 还是 Compaction。

主要调用链:

MakeRoomForWrite -> MaybeScheduleCompaction -> BackgroundCall ->

BackgroundCompaction -> CompactMemTable -> WriteLevel0Table

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值