LevelDB源分析之compaction过程

本文深入探讨LevelDB的compaction过程,分析了LSM树如何提升写性能,以及为何需要多层level设计以优化读性能。通过源码分析,揭示了compaction触发时机、 SST选择、具体合并步骤及新Version的生成过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. LevelDB 简介

LevelDB 是 Google 开源的一款单机 KV 持久存储引擎,可以看作是BigTable 的单机版本。通过 LSM 树(Log Structured Merge Tree),LevelDB 牺牲了一定的读性能,但提高了写性能。对外 LevelDB 提供了常用的 PutGetDelete 等接口,还支持 Snapshot、Recovery 等操作。

本篇文章主要分析 LevelDB 的 compaction 过程以及分层设计的思想,对于 LevelDB 不熟悉的同学,可以先查看其他资料。

2. LSM 树在 LevelDB 中的应用

我们知道直接在磁盘中进行随机写性能是比较低的,那么为了获得较好的随机写性能,LevelDB 使用 LSM 树来组织数据。我们首先看下写流程:

当有一条记录要写入磁盘时,LevelDB 首先会将本次写操作 Append 到日志中(相当于顺序写),然后把这条记录插入内存中的 Memtable 中(Memtable 底层使用 skiplist 来保证记录有序),可见 LevelDB 将随机写转化为了顺序写。

Memtable 达到一定大小时,就会被 dump 到磁盘,这里有两个原因:1. 内存是有限的,不能将数据都放在内存中;2. 断电后内存中的数据就会全部丢失,虽然有日志可以恢复数据,但每次都恢复全部的数据显然不现实。

Memtable 一般情况下会被 dump 到 level 0,level L 中 tables 总大小超过一定大小时,LevelDB 就会在 level L 中选取一些 table 与 level L+1 中有 key 重叠的 table 进行 compaction,生成新的 table。这里我们思考一下为什么需要设计多层 level?如果只有 level 0,随着 Memtable 不断被 dump 到磁盘上,level 0 上的 table 数量就会越来越多,并且这些 tables 的 key 存在相互重叠的情况。那么每次读操作时,如果在 Memtable 中没有找到数据,就需要在所有 tables 中进行查找,显然读性能受到了严重影响。

为了解决这一问题我们可以考虑限制 level 0 上 tables 的数量,同时增加一个 level 1,当 level 0 上 tables 数量超过一定大小时,就选取一些相互重叠的 tables 将其合并到 level 1,这样 level 1 中 tables 的 key 是相互不重叠的。此时我们最多只需要读 N+1 个文件,而 N 是比较小的,这样就达到了较好的读性能。

从上面的分析中似乎只要设计两层 level 就可以了,那为什么 LevelDB 还要多层的设计呢?设想一下随着 compaction 次数的增加,level 1 上的 tables 数量也会变得很多,这样极端情况下,level 0 上的一个 table 的 key range 可能会和 level 1 上所有tables 相重合,这样在进行 compaction 时就需要合并 level 1 上所有的 table,IO 开销太大。所以为了降低 compaction 时的 IO 开销,需要进行分层的设计。

3. compaction 过程源码分析

3.1 compaction 触发时机

读写数据时都会触发 compaction ,我们首先看下写数据时的触发流程:

写数据时主要调用DBImpl::Write()接口,在Write()接口中会调用MakeRoomForWrite()接口,我们看下这个接口的内部逻辑:

Status DBImpl::MakeRoomForWrite(bool force) { //写数据时该值为false
  mutex_.AssertHeld();
  assert(!writers_.empty());
  bool allow_delay = !force; //true
  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) {
        //L0文件达到8个时会减慢写入速度,延迟1ms
      // 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)) {
      // memtable的大小还没有达到指定size,可以直接往里写数据
      // There is room in current memtable
      break;
    } else if (imm_ != nullptr) { 
       // 走到这一步说明memtable中已经没有空间可以写数据了,需要将memtable转化为immutable
       // 但是immutable不为空,表明后台正在进行compact,需要等待compaction完成再进行转化
      // 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) {
       // level 0上的文件数过多,需要compaction
      // There are too many level-0 files.
      Log(options_.info_log, "Too many L0 files; waiting...\n");
      background_work_finished_signal_.Wait();
    } else {
      //走到这一步表明可以将memtable转化为immutable了
      // Attempt to switch to a new memtable and trigger compaction of old
      assert(versions_->PrevLogNumber() == 0);
      uint64_t new_log_number = versions_->NewFileNumber();
      Wri
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值