RocksDB. MemTable源码分析

MemTable

  • MemTable是一个内存中数据结构,用来保存新写入的还没有flush到SST文件中的数据。
  • 读写请求都会经过MemTable
  • 新写入的数据都会插入到MemTable中
  • 读请求先查询MemTable,再查询SST文件
  • 一旦MemTable被写满了,它就变为不可写,并创建一个新的MemTable用来服务
  • 由一个后台线程,将已经满的MemTable flush到SST文件中,flush完成后,销毁该MemTable

选项

  • memtable_factory
    memtable工厂类,用来指定不同的memtable实现。
  • write_buffer_size
    一个memtable的大小.
  • db_write_buffer_size
    所有colume families的memtable的总大小, 可以用来限制memtable可以使用的总内存.
  • write_buffer_manager
    除了可以使用上面的参数来控制memtable使用的内存, 用户还可以定义自己的buffer manager, 来控制memtable可以使用的总内存
  • max_write_buffer_number
    内存中可以保留的memtable的最大数量

memtable使用的默认实现是基于skiplist.

不同实现下MemTable的特点

1 Skiplist MemTable

基于skiplist实现的memtable为读, 写, 随机访问和顺序访问, 都提供了很好的性能.
而且, 它还提供了一些有用的功能, 像concurrent insert和Insert with Hint.

2 HashSkiplist MemTable

HashSkipList维护一个hash table, 每个hash桶都是一个skip list.
这样做的目的是在查询的时候, 减少比较的次数.
分桶是通过对key的前缀进行hash, 根据hash值找到对应的桶. 在skiplist内, 对整个键进行比较.
HashSkipList的限制在于, 如果需要在多个前缀中遍历, 需要拷贝和排序操作, 非常慢, 而且耗内存.

在代码中还有两种类型的MemTable的实现, HashCuckooRep和VectorRep.
本篇主要分析基于SkipList的MemTable的实现.

MemTable类图

MemTable类图

职责说明

  1. MemTable
    维护在内存中,还没有刷盘的用户数据,底层实现为skiplist,vector等数据结构,对外提供Add、Get、Update等结构,接受的数据为key - value形式。
  2. MemTableRepFactory
    工厂接口类,用于创建指定类型的MemTableRep对象。
  3. SkipListFactory
    工厂类,用于创建SkipListRep对象。
  4. MemTableRep
    接口类,定义了底层数据结构提供给MemTable的接口。主要包括Allocate、Insert族、Get和Contain接口。
  5. SkipListRep
    基于SkipList的MemTableRep子类。
  6. MemTableRep::Iterator
    迭代器,提供常规的迭代器接口,在MemTable对象中,由MemTableIterator内部类持有和调用。

MemTable插入流程分析

调用场景

在写流程中,会调用MemTableInserter的PutCF接口,在该接口中,首先检查column family的合法性,然后获取当前可写的memtable,调用MemTable的Add方法,将key value插入到memtable中。

  virtual Status PutCF(uint32_t column_family_id, const Slice& key,
                       const Slice& value) override {
    ...
    Status seek_status;
    if (!SeekToColumnFamily(column_family_id, &seek_status)) {
      ++sequence_;
      return seek_status;
    }

    MemTable* mem = cf_mems_->GetMemTable();
    auto* moptions = mem->GetMemTableOptions();
    if (!moptions->inplace_update_support) {
      mem->Add(sequence_, kTypeValue, key, value, concurrent_memtable_writes_,
               get_post_process_info(mem));
    }
    ...

这里并没有锁保护,MemTable本身并不是线程安全的,所以大部分时候,需要外部的同步机制来提供锁。在写流程中提供了必须的锁保护。

Add接口的实现

Add接口将指定sequence number和类型的key和value合并成一个特定格式的entry,插入到memtable中。
接口声明

  void Add(SequenceNumber seq, ValueType type, const Slice& key,
           const Slice& value, bool allow_concurrent = false,
           MemTablePostProcessInfo* post_process_info = nullptr);

合并后的一条数据格式如下

| internal_key_size: 包括 key + type的长度 | key | seqid + type | value size | value |

实现细节如下:

  1. 由skiplist持有的Arena对象来对一条记录分配指定长度的内存buf
  2. 将internal+key_size用变长编码保存到指定内存中
  3. 将key拷贝到指定内存中
  4. 将sequence number和type合并到一个64位整数中,type保存在seqence number的低8位中。将合并后的结果保存到内存中
  5. 将val_size用变长编码保存到内存中
  6. 拷贝value到内存中
  uint32_t key_size = static_cast<uint32_t>(key.size());
  uint32_t val_size = static_cast<uint32_t>(value.size());
  // +8 是在key的后面用8位保存数据类型type字段
  uint32_t internal_key_size = key_size + 8;
  const uint32_t encoded_len = VarintLength(internal_key_size) +
                               internal_key_size + VarintLength(val_size) +
                               val_size;
  char* buf = nullptr;
  std::unique_ptr<MemTableRep>& table =
      type == kTypeRangeDeletion ? range_del_table_ : table_;
  KeyHandle handle = table->Allocate(encoded_len, &buf);

  char* p = EncodeVarint32(buf, internal_key_size);
  memcpy(p, key.data(), key_size);
  Slice key_slice(p, key_size);
  p += key_size;
  uint64_t packed = PackSequenceAndType(s, type);
  EncodeFixed64(p, packed);
  p += 8;
  p = EncodeVarint32(p, val_size);
  memcpy(p, value.data(), val_size);
  assert((unsigned)(p + val_size - buf) == (unsigned)encoded_len);

将一条记录合并完成之后,插入到真正存储数据的结构中,本文以skiplist的插入为例。
MemTable持有一个Rep封装成员变量,类型为MemTableRep,提供了底层数据结构统一的接口。

unique_ptr<MemTableRep> table_;

在MemTable的构造函数中,通过工厂方法来初始化该成员。

MemTable::MemTable(const InternalKeyComparator& cmp,
                   const ImmutableCFOptions& ioptions,
                   const MutableCFOptions& mutable_cf_options,
                   WriteBufferManager* write_buffer_manager,
                   SequenceNumber latest_seq)
    : comparator_(cmp),
      moptions_(ioptions, mutable_cf_options),
      refs_(0),
      kArenaBlockSize(OptimizeBlockSize(moptions_.arena_block_size)),
      arena_(moptions_.arena_block_size,
             mutable_cf_options.memtable_huge_page_size),
      allocator_(&arena_, write_buffer_manager),
      table_(ioptions.memtable_factory->CreateMemTableRep(
          comparator_, &allocator_, ioptions.prefix_extractor,
          ioptions.info_log)),
      ...

在Add方法中,调用MemTableRep提供的Insert族方法将一条数据插入到数据结构中。

  ...
  std::unique_ptr<MemTableRep>& table =
      type == kTypeRangeDeletion ? range_del_table_ : table_;
  ...
  if (!allow_concurrent) {
    // Extract prefix for insert with hint.
    if (insert_with_hint_prefix_extractor_ != nullptr &&
        insert_with_hint_prefix_extractor_->InDomain(key_slice)) {
      Slice prefix = insert_with_hint_prefix_extractor_->Transform(key_slice);
      table->InsertWithHint(handle, &insert_hints_[prefix]);
    } else {
      table->Insert(handle);
    }
  ...
  } else {
    ...
    table->InsertConcurrently(handle);
    ...
  }

skiplist的原理,可以参考wiki
rocksdb有两个skiplist的实现

  • SkipList
  • InlineSkipList

其中InlineSkipList实现的非常精细,打算重新开一篇分析。
在这里,上面合并之后的记录,最终插入到了skiplist中

  virtual void Insert(KeyHandle handle) override {
    skip_list_.Insert(static_cast<char*>(handle));
  }

  virtual void InsertWithHint(KeyHandle handle, void** hint) override {
    skip_list_.InsertWithHint(static_cast<char*>(handle), hint);
  }

  virtual void InsertConcurrently(KeyHandle handle) override {
    skip_list_.InsertConcurrently(static_cast<char*>(handle));
  }

小结

MemTable是RockDB中非常重要的数据,用于在内存中维护插入的数据,并且一些重要的功能,如prefix seek,concurrent insert,都对它有依赖。
本篇主要简要介绍MemTable的基本功能和实现,更多的细节在分析其他功能时,还会回来更深入的了解。


参考资料:
https://github.com/facebook/rocksdb/wiki/MemTable

推荐阅读更多精彩内容