LevelDB memtable 原理和源码分析

本文首先介绍了 LSM Tree,跳表,然后介绍 memtable 和 LevelDB 的源码实现,memtable 是 LSM Tree 放在内存的数据结构,它保存了最近发生的写入操作,且具有 O(logn) 的查找和插入性能。

Why LSM Tree

The Pathologies of Big Data 提到,硬盘随机读写与顺序读写吞吐率至少相差三个数量级。而且,对硬盘的顺序读写甚至比主存的随机访问还快。

硬盘随机读写与顺序读写吞吐率对比:
硬盘随机读写与顺序读写吞吐率对比

如果仅用一个日志顺序记录发生的写操作,那么就可以充分利用硬盘顺序读写吞吐率高的特性。但实际环境中,往往要支持随机读查询和范围查询。

为了支持这些操作,就要引入诸如 B+ 树的数据结构。将数据按 B+ 树的结构组织起来然后持久化,这引入了硬盘的随机读写,失去了硬盘顺序读写的优势。而且,进行数据更新时,要顺着 B+ 树索引到目标数据,这就需要多次 random IO。

另一种方法是使用哈希表对键进行映射,将一个键映射到与其相关的、最近的 Log Entry 在硬盘上的位置。这时查找一个键只需要一次 random IO。但这种方法会有扩展性问题,如果数据流中有许多很小的键,那么可能内存中的哈希表比在硬盘上的所有键值对加起来还大。

由此,我们引出 LSM Tree。LSM Tree 数据写入充分利用了硬盘顺序 IO 速度快,随机 IO 速度慢的特性,并且它也提供了不俗的读取性能。

What is LSM Tree

在 LSM tree 中,一批批写操作被顺序保存到一个个索引文件中,所以每个文件覆盖了一小段时间内发生的修改。每个文件在写到硬盘前,会先对其进行排序,于是,在这些文件中进行查找只需 O(logn)。这些文件是不可变的,它们不会再被更新,新发生的更新会被写到新的文件中。读操作会检查所有的文件,直至找到关于特定键的最新的写操作。写到硬盘的文件会被定期合并,减少空间占用。

当发生一次更新操作,会将这个更新放到内存中的 memtable 中缓存。为了维护有序性,这些更新操作在内存中一般被组织成红黑树或跳表。同时,这些更新操作被顺序写到硬盘上的日志文件中,以供 crash 时恢复之用。当写入的内容达到阈值,这个 memtable 将被刷到硬盘上,并创建新的 memtable,继续处理新的写操作。

因为对一个键的写操作会被写到多个有序文件中,我们可以对这些写操作进行合并,比如键 x 发生了三个写操作:

  • x = 1
  • x = 2
  • x = 3

只需要 x = 3 这条记录就足以保存键 x。于是,LSM Tree 会定期进行压缩。LSM Tree 会选择多个有序文件,并将它们合并到一起,将一个键的多次更新、删除操作合并。压缩操作有两个用处:一是节约存储空间,二是防止因为文件增多而造成读性能降级。

当发生一个读请求,系统先检查 memtable,如果没有找到,将会按时间逆序逐个查找硬盘上的文件,直至找到要找的键。

这种方法在文件越来越多时,读操作会越来越慢。有几种方法可以加速读操作。最常用的就是将一个页索引放在内存中,这个索引能够让系统定位到目标键的附近,系统能从索引指示的位置开始扫描。LevelDB 通过在每个文件中保存块缓存,进一步加速了一个文件内的查找。这种方法比二分查找更好,因为一个键值对的大小不一定是固定的,且这种方法更适合压缩后的数据。

LSM Tree 结构

Size-tiered compaction

LSM Tree 通用的 Compaction 策略叫做 size-tiered compaction。size-tiered compaction 的思路是:每层允许的 SST 文件最大数量都有个相同的阈值,随着不断将 memtable 写为 SST,当某层的 SST 数达到阈值时,就把该层所有 SST 全部合并成一个大的新 SST,并放到较高一层去。

这种策略下,同一层的各个 SST 有可能包含相同的 key。

在存在大量更新操作的工作流中,size-tiered compaction 会有以下问题:

  • 性能不稳定,因为不能保证一个 entry 出现在几个 sstable,这就导致 Read Amplification;
  • 空间浪费严重,因为同一层也存在许多重复的键,这个问题在删除操作较多时更为突出;

Leveled compaction

Leveled compaction 的思路是:对于 L1 层及以上的数据,将 size-tiered compaction 中原本的大 SST 拆开,成为多个 key 互不相交的 SST 的序列,这样的序列叫做“run”。L0 层是从 memtable flush 过来的新 SST,该层各个 SST 的 key 是可以相交的,并且其数量阈值单独控制(如4)。从L1层开始,每层都包含恰好一个 run,并且 run 内包含的数据量阈值呈指数增长。Leveled compaction 可以有效地缓解 size-tiered compaction 的 Space Amplification 和 Read Amplification。

leveled compaction

Leveled compaction 带来的问题是 Write Amplification。假设 L2 中的一个 sstable 与 L3 中的所有 sstable 都有交集,为了将 L2 中这个 sstable 合并到 L3,必须读取 L3 的所有 sstable。

Hybrid

前 k 层使用 size-tiered compaction,之后的层用 leveled compaction。

LSM Tree in LevelDB

LevelDB 是一个 google 实现的非常高效的 KV 数据库。

LevelDB 采用了 LSM Tree 实现了高效的写操作和不错的读性能。LSM tree 主要由两部分数据结构组成,其一是驻留内存的 memtable,其二是保存到磁盘上的 sstable

  • LevelDB 在内存中维护 memtable 记录最近的写操作(kTypeValue)和删除操作(kTypeDeletion);
  • 当内存中的 memtable 达到一定大小后,会将其保存为硬盘上的 sstable,并放到 Level-0.

LevelDB 的 memtable 是通过 Skip List 对键进行组织的。本文分析 LevelDB 的 memtable 实现。

LevelDB 中 L0 的 sstable 之间 key 有重叠,L1-L6 每一层的 sstable 间键都是有序的,通过 compaction 机制来保证的

Skip List

memtable 维护最近发生在 LevelDB 的写操作。其底层采用的是一种具有 O(logn) 查找和插入时间复杂度的数据结构:Skip List,以下简单介绍其概念。

Skip List 有多个层,其最底层是一个排好序的链表。其结构可以由下图表示。每个节点以某一概率 p 向上延伸。

通过级数求和可以得出每个结点平均有 1/(1-p) 层中。

skip list

Skip list 的查找过程

  • 如上图所示,假设要查找 9,查找时,从顶层开始,找到第一个恰小于 9 的结点,这里为 1;
  • 因为结点 1 后没有更多结点,向下走一层,重复上述过程,到达结点 6;
  • 继续往下走一层,然后往前找,到达 9,即找到目标结点。

Skip list 的插入

Skip list 的插入过程与查找类似。找到插入位置后,将新节点插入链表中,新节点以某一概率 p 向上延伸。

memtable 实现

以下分析 LevelDB 的 memtable 源码实现。

MemTable 类的 API 总览

MemTable 位于 db/memtable.h,其 API 如下:

  // 自增引用计数。
  void Ref() { ++refs_; }

  // 引用计数减一,如果引用计数为零则将这个 memtable 删除。
  void Unref() {
    --refs_;
    assert(refs_ >= 0);
    if (refs_ <= 0) {
      delete this;
    }
  }

  // 返回此 memtable 大约占用的内存空间大小。
  size_t ApproximateMemoryUsage();

  // memtable 的迭代器。
  // 调用者需要保证在迭代器存活时 memtable 也存活。
  // 此迭代器返回的是编码后的内部键(internal key)。
  Iterator* NewIterator();

  // 往 memtable 中添加一个键值对,指定序列号 SequenceNumber 和类型。
  // 类型为 kTypeValue 表明是插入键值对,类型为 kTypeDeletion 表明是删除键值对。
  void Add(SequenceNumber seq, ValueType type, const Slice& key,
           const Slice& value);

  // 根据键从 memtable 获取值,值将被存到 value 变量中,并返回 true。
  // 如果 memtable 包含一个 key 的删除标记,存一个 NotFound() 到变量 s 中并返回 true。
  // 其它情况返回 false。
  bool Get(const LookupKey& key, std::string* value, Status* s);

MemTable 的成员变量如下:

  // 为保存数据的 skip list 取一个别名
  typedef SkipList<const char*, KeyComparator> Table;
  
  // 键的比较器
  KeyComparator comparator_;
  // 引用计数器
  int refs_;
  // 内存分配器
  Arena arena_;
  // 底层的 skip list
  Table table_;

可以看到,在 memtable.h 中,保存数据的 Table 类型实际上是一个 Skip List。有了 Skip List,memtable 只需处理键值对内存的分配、键值对 entry 的编码、解码等工作。

Add()

Add将一个entry插入到memtable中。

一个entry的内存布局如下:

entry内存布局

此处 key 类型为 Internal Key。LevelDB 包含三种 key:

  • Lookup Key
  • Internal Key
  • User Key

User Key 是读写数据库直接用到的 key,一般用一个 Slice 表示,如:

db->Put(leveldb::WriteOptions(), "hello", "LevelDB");	// 此处 "hello" 即 User Key.

Internal Key 则是在 User Key 的基础上扩充了两个字段。Lookup Key 又在 Internal Key 的首部添加了一个字段。它们三者的关系如下图所示:

三种Key

Add代码实现如下:

void MemTable::Add(SequenceNumber s, ValueType type, const Slice& key,
                   const Slice& value) {
  // 入参 key 是上述 User Key,编码后的 entry 内存布局如下:
  //  key_size     : varint32 of internal_key.size()
  //  key bytes    : char[internal_key.size()]
  //  tag          : uint64((sequence << 8) | type)
  //  value_size   : varint32 of value.size()
  //  value bytes  : char[value.size()]
  // +------------+-------+-------+--------------+---------+
  // |  key_size  |  key  |  tag  |  value_size  |  value  |
  // +------------+-------+-------+--------------+---------+
  
  size_t key_size = key.size();
  size_t val_size = value.size();

  // Internal Key 占用内存大小为 User Key + 8 bytes,详见上图
  size_t internal_key_size = key_size + 8;
  
  // 计算编码所需字节数
  const size_t encoded_len = VarintLength(internal_key_size) +
                             internal_key_size + VarintLength(val_size) +
                             val_size;
  // 给 entry 分配空间。
  char* buf = arena_.Allocate(encoded_len);
  
  // 将 key_size 写到此 entry 中。
  char* p = EncodeVarint32(buf, internal_key_size);
  
  // 将 key 写到此 entry 中。
  std::memcpy(p, key.data(), key_size);
  p += key_size;
  
  // 将 tag 写到此 entry 中,tag 包含序列号的低 56 位和操作类型(插入/删除,八位)。
  EncodeFixed64(p, (s << 8) | type);
  p += 8;
  
  // 将 val_size 写到此 entry 中。
  p = EncodeVarint32(p, val_size);
  
  // 将 value 写到此 entry 中。
  std::memcpy(p, value.data(), val_size);
  
  assert(p + val_size == buf + encoded_len);
  // entry 构建完成,将它插入到 skip list 中。
  table_.Insert(buf);
}

Add 实际上分配了一块内存,然后通过 EncodeVarint32EncodeFixed64 函数将键大小、键、tag、值大小、值编码到这块内存中,然后调用其 skip list 成员的方法 table_.Insert(buf) 将构造的 entry 插入到 skip list 中。

我们看 EncodeFixed64 是怎么对数据进行编码的:

inline void EncodeFixed64(char* dst, uint64_t value) {
  uint8_t* const buffer = reinterpret_cast<uint8_t*>(dst);

  // Recent clang and gcc optimize this to a single mov / str instruction.
  // 低地址存低位,高地址存高位
  buffer[0] = static_cast<uint8_t>(value);
  buffer[1] = static_cast<uint8_t>(value >> 8);
  buffer[2] = static_cast<uint8_t>(value >> 16);
  buffer[3] = static_cast<uint8_t>(value >> 24);
  buffer[4] = static_cast<uint8_t>(value >> 32);
  buffer[5] = static_cast<uint8_t>(value >> 40);
  buffer[6] = static_cast<uint8_t>(value >> 48);
  buffer[7] = static_cast<uint8_t>(value >> 56);
}

实际上,就是将 64 位的 unsigned int 的内存放到了 buffer 中。假设有以下 uint64_t:

10000000 11000000 11100000 11110000 11111000 11111100 11111110 11111111

那么编码后的 dst 的内存分布为:

dst = [11111111, 11111110, 11111100, 11111000, 11110000, 11100000, 11000000, 10000000]

所以,LevelDB 中,EncodeFixed64 使用小端存数据。

再看 EncodeVarint32 是怎么编码数据的:

char* EncodeVarint32(char* dst, uint32_t v) {
  // 将 dst 解释为 uint8 数组
  uint8_t* ptr = reinterpret_cast<uint8_t*>(dst);
  // 以
  static const int B = 128;
  if (v < (1 << 7)) {
    *(ptr++) = v;
  } else if (v < (1 << 14)) {
    *(ptr++) = v | B;
    *(ptr++) = v >> 7;
  } else if (v < (1 << 21)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = v >> 14;
  } else if (v < (1 << 28)) {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = v >> 21;
  } else {
    *(ptr++) = v | B;
    *(ptr++) = (v >> 7) | B;
    *(ptr++) = (v >> 14) | B;
    *(ptr++) = (v >> 21) | B;
    *(ptr++) = v >> 28;
  }
  return reinterpret_cast<char*>(ptr);
}

EncodeVarint32EncodeFixed64 不同,它使用的是大端存储(高地址存低位,低地址存高位),它会以能够容纳 v 的内存大小编码 v,这个内存大小可以为 7, 14, 21, 28, 32 位。

Get()

Get 根据入参的 LookupKeymemtable 中查找一个 entry。

Get 代码及解析如下:

bool MemTable::Get(const LookupKey& key, std::string* value, Status* s) {
  Slice memkey = key.memtable_key();
  // 声明了一个 skip list 上的迭代器。
  Table::Iterator iter(&table_);
  iter.Seek(memkey.data());
  if (iter.Valid()) {
    // entry format is:
    //    klength  varint32
    //    userkey  char[klength]
    //    tag      uint64
    //    vlength  varint32
    //    value    char[vlength]
    // 检查它属于同一个 user key。
    // 不检查序列号,因为 Seek() 已经跳过了序列号过大的 entry。
    
    // 迭代下一个键。
    const char* entry = iter.key();
    // 从 entry 中读出 key_length。
    uint32_t key_length;
    // GetVarint32Ptr 从 entry 读出 key_length,并返回读取后指针的位置,即 key 的地址。
    const char* key_ptr = GetVarint32Ptr(entry, entry + 5, &key_length);
    // 对 entry 中的 user key 和 lookup key 中的 user key 进行比较。
    if (comparator_.comparator.user_comparator()->Compare(
            Slice(key_ptr, key_length - 8), key.user_key()) == 0) {
      // 利用比较器比较相等,找到了正确的 user key.
      // 取出紧跟 user key 后的 tag,即序列号和值类型。
      const uint64_t tag = DecodeFixed64(key_ptr + key_length - 8);
      // 取出 tag 低 8 位的值,即 value type.
      switch (static_cast<ValueType>(tag & 0xff)) {
        case kTypeValue: {
          // key_ptr + key_length 即值长度字段的位置,GetLengthPrefixedSlice 从 entry 里面取出了值 v。
          Slice v = GetLengthPrefixedSlice(key_ptr + key_length);
          // 将值 v 赋给 value 后返回 true.
          value->assign(v.data(), v.size());
          return true;
        }
        case kTypeDeletion:
          // tag 指示键已经被删除,返回 not found.
          *s = Status::NotFound(Slice());
          return true;
      }
    }
  }
  return false;
}

memtable 写到 sstable 的过程

当一个 memtable 超出一定的大小(默认4MB)时,会创建一个新的 memtable 和日志文件,并将以后的更新定向到此新的 memtable 和日志文件。

memtable 大小触及阈值后发生的后台工作

在后台进行以下工作:

  1. 将先前的 memtable 内容写到 sstable
  2. 丢弃旧的 memtable
  3. 删除旧的日志文件和 memtable
  4. 将新的 sstable 添加到 level-0。

同步工作流

首先,用户调用 Put(),将键值对存入数据库:

// Default implementations of convenience methods that subclasses of DB
// can call if they wish
Status DB::Put(const WriteOptions& opt, const Slice& key, const Slice& value) {
  WriteBatch batch;
  batch.Put(key, value);
  return Write(opt, &batch);
}

上述 WriteBatch 表示一批写操作,LevelDB 支持原子地将一批数据写入。WriteBatch::Put 方法如下:

void WriteBatch::Put(const Slice& key, const Slice& value) {
  // 设置这个 batch 的写操作数量
  WriteBatchInternal::SetCount(this, WriteBatchInternal::Count(this) + 1);
  // 将数据类型、键值对存到 rep_ 中
  rep_.push_back(static_cast<char>(kTypeValue));
  PutLengthPrefixedSlice(&rep_, key);
  PutLengthPrefixedSlice(&rep_, value);
}

// write_batch.h
class LEVELDB_EXPORT WriteBatch {
  ...
  std::string rep_;
}

设置好 WriteBatch 后,DB::Put 调用 DBImpl::Write。该方法创建了一个 Writer,将它压入 writers_ 队列中,然后通过信号量等待排在前面的写操作完成。

// 1. 通关 MakeRoomForWrite 为写操作让出空间
// 2. 将相应的日志写入并刷盘
// 3. 将新的键值对写入 memtable
Status DBImpl::Write(const WriteOptions& options, WriteBatch* updates) {
  Writer w(&mutex_);
  w.batch = updates;
  w.sync = options.sync;
  w.done = false;

  MutexLock l(&mutex_);
  writers_.push_back(&w);
  // 如果前面还有写操作,阻塞等待唤醒
  while (!w.done && &w != writers_.front()) {
    w.cv.Wait();
  }
  if (w.done) {
    return w.status;
  }

  // 当前在 writers_ 队头,开始执行写操作
  
  // 为写操作让出空间,这可能触发
  Status status = MakeRoomForWrite(updates == nullptr);
  
  uint64_t last_sequence = versions_->LastSequence();
  Writer* last_writer = &w;
  if (status.ok() && updates != nullptr) {  // nullptr batch is for compactions
    WriteBatch* write_batch = BuildBatchGroup(&last_writer);
    WriteBatchInternal::SetSequence(write_batch, last_sequence + 1);
    last_sequence += WriteBatchInternal::Count(write_batch);

    // 将键值对添加到日志中,并应用到 memtable 上。
    {
      mutex_.Unlock();
      status = log_->AddRecord(WriteBatchInternal::Contents(write_batch));
      bool sync_error = false;
      if (status.ok() && options.sync) {
        // 将日志刷盘
        status = logfile_->Sync();
        if (!status.ok()) {
          sync_error = true;
        }
      }
      if (status.ok()) {
        // 将键值对插入到 memtable 中
        status = WriteBatchInternal::InsertInto(write_batch, mem_);
      }
      mutex_.Lock();
      if (sync_error) {
        // The state of the log file is indeterminate: the log record we
        // just added may or may not show up when the DB is re-opened.
        // So we force the DB into a mode where all future writes fail.
        RecordBackgroundError(status);
      }
    }
    if (write_batch == tmp_batch_) tmp_batch_->Clear();

    versions_->SetLastSequence(last_sequence);
  }

  // 为什么前面还有不是 &w 的 writer?
  while (true) {
    Writer* ready = writers_.front();
    writers_.pop_front();
    if (ready != &w) {
      ready->status = status;
      ready->done = true;
      ready->cv.Signal();
    }
    if (ready == last_writer) break;
  }

  // 唤醒下一个阻塞的写
  if (!writers_.empty()) {
    writers_.front()->cv.Signal();
  }

  return status;
}

DBImpl::MakeRoomForWrite 意即为写操作腾出空间,它会根据当前 Level-0 的文件数、memtable 的大小、压缩工作的进展等作出相应的反应,其代码如下:

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) {
      // 已经接近 L0 文件数的硬上限,为了不让某一个触及上限的写操作产生很长的延迟,让当前线程休眠一会儿。
      // 如果当前写线程和压缩线程共用一个 CPU,这个延迟可以将 CPU 让出给压缩线程。
      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 还有空间,跳出循环返回。
      break;
    } else if (imm_ != nullptr) {
      // memtable 满了,但前一个 memtable 还在压缩当中,阻塞等待后台线程完成压缩。
      Log(options_.info_log, "Current memtable full; waiting...\n");
      background_work_finished_signal_.Wait();
    } else if (versions_->NumLevelFiles(0) >= config::kL0_StopWritesTrigger) {
      // Level-0 文件数量太多,阻塞等待后台线程完成合并。
      Log(options_.info_log, "Too many L0 files; waiting...\n");
      background_work_finished_signal_.Wait();
    } else {
      // 换到一个新的 memtable,并触发对旧 memtable 的压缩。
      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;
      }

	  // 删除旧日志对象,这里只是删除内存中的日志对象,真正的 WAL 已经被刷盘。
      delete log_;

      s = logfile_->Close();
      if (!s.ok()) {
        // We may have lost some data written to the previous log file.
        // Switch to the new log file anyway, but record as a background
        // error so we do not attempt any more writes.
        //
        // We could perhaps attempt to save the memtable corresponding
        // to log file and suppress the error if that works, but that
        // would add more complexity in a critical code path.
        RecordBackgroundError(s);
      }
      delete logfile_;

      logfile_ = lfile;
      logfile_number_ = new_log_number;
      log_ = new log::Writer(lfile);
      // 将旧 memtable 赋给 imm_,然后让后台线程将它写到 sstable.
      imm_ = mem_;
      has_imm_.store(true, std::memory_order_release);
      // 创建新的 memtable
      mem_ = new MemTable(internal_comparator_);
      mem_->Ref();
      force = false;  // Do not force another compaction if have room
      MaybeScheduleCompaction();
    }
  }
  return s;
}


// size_t write_buffer_size = 4 * 1024 * 1024;

由上述函数,当 memtable 的大小达到 write_buffer_size 就触发 Compaction,MakeRoomForWrite 实际只是将旧的 memtable 放到自己的成员 imm_ 上,然后就创建新的 memtable。说明将 memtale 写到 Level-0 并不是由写线程完成的。注意到最后调用了 MaybeScheduleCompaction(),该方法能够调度后台线程进行 Compaction.

异步工作流

进入了 MaybeScheduleCompaction 之后,将会把 Compaction 工作交给相应的后台线程。下面分析 Write 是怎样将 Compaction 工作交给后台线程的,以及 Compaction 完成后进行了哪些操作。

void DBImpl::MaybeScheduleCompaction() {
  mutex_.AssertHeld();
  if (background_compaction_scheduled_) {
    // Already scheduled
  } 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()) {
    // No work to be done
  } else {
    background_compaction_scheduled_ = true;
    env_->Schedule(&DBImpl::BGWork, this);	// 调度运行 BGWork
  }
}

DBImpl::BGWork 实际调用 DBImpl::BackgroundCall

void DBImpl::BGWork(void* db) {
  reinterpret_cast<DBImpl*>(db)->BackgroundCall();
}

BackgroundCall() 进一步调用了 BackgroundCompaction() 对数据进行压缩。

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;

  // 前一个 Compaction 可能在下一层产生太多文件,再调度一次 Compaction.
  MaybeScheduleCompaction();
  // 压缩完成,唤醒所有等待信号的线程。
  background_work_finished_signal_.SignalAll();
}

上述调度方法实际上是将工作放到一个队列 background_work_queue_ 中,后台线程再从队列中取出工作执行,实际上是一个生产者-消费者模型

DBImpl::BackgroundCompaction 里面调用了 DBImpl::CompactMemTablememtable 进行压缩:

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

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

DBImpl::CompactMemTable() 实现如下:

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

  // 将 memtable 的内容存到一个 sstable 文件
  VersionEdit edit;
  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);
    edit.SetLogNumber(logfile_number_);  // Earlier logs no longer needed
    s = versions_->LogAndApply(&edit, &mutex_);
  }

  if (s.ok()) {
    // 已经将 memtable 写入到 sstable,Unref 将导致旧 memtable 的释放。
    imm_->Unref();
    imm_ = nullptr;
    has_imm_.store(false, std::memory_order_release);
    
    // 移除废弃的文件,包括已经写入到 sstable 后不需要的 WAL。
    RemoveObsoleteFiles();
  } else {
    RecordBackgroundError(s);
  }
}

它调用了 WriteLevel0Table,将 memtable 写到 Level-0:

Status DBImpl::WriteLevel0Table(MemTable* mem, VersionEdit* edit,
                                Version* base) {
  mutex_.AssertHeld();
  const uint64_t start_micros = env_->NowMicros();
  FileMetaData meta;
  meta.number = versions_->NewFileNumber();
  pending_outputs_.insert(meta.number);
  // 拿到 memtable 的一个迭代器
  Iterator* iter = mem->NewIterator();
  Log(options_.info_log, "Level-0 table #%llu: started",
      (unsigned long long)meta.number);

  Status s;
  {
    mutex_.Unlock();
    // 用 memtable 的迭代器及一些元数据构造 sstable
    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);

  ...
}

上述代码中,BuildTable 完成了由 memtable 构造 sstable

小结

memtable 大小触及阈值后发生的几件事情都分析到了:

  • 将旧 memtable 交给后台线程写到 Level-0 sstable,调用最终流转到了 WriteLevel0Table
  • 丢弃并创建新的 memtable,这发生在 MakeRoomForWrite 中;
  • 删除日志文件主要发生在 CompactMemTable 中调用的 RemoveObsoleteFiles,旧 memtable 在压缩完成后通过 Unref 释放。

参考资料

introduction to LSM
LevelDB Documentation
Skip List Wikipedia

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值