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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容