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
维护在内存中,还没有刷盘的用户数据,底层实现为skiplist,vector等数据结构,对外提供Add、Get、Update等结构,接受的数据为key - value形式。 - MemTableRepFactory
工厂接口类,用于创建指定类型的MemTableRep对象。 - SkipListFactory
工厂类,用于创建SkipListRep对象。 - MemTableRep
接口类,定义了底层数据结构提供给MemTable的接口。主要包括Allocate、Insert族、Get和Contain接口。 - SkipListRep
基于SkipList的MemTableRep子类。 - 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 |
实现细节如下:
- 由skiplist持有的Arena对象来对一条记录分配指定长度的内存buf
- 将internal+key_size用变长编码保存到指定内存中
- 将key拷贝到指定内存中
- 将sequence number和type合并到一个64位整数中,type保存在seqence number的低8位中。将合并后的结果保存到内存中
- 将val_size用变长编码保存到内存中
- 拷贝value到内存中
uint32_t key_size = static_cast(key.size());
uint32_t val_size = static_cast(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& 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 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& 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(handle));
}
virtual void InsertWithHint(KeyHandle handle, void** hint) override {
skip_list_.InsertWithHint(static_cast(handle), hint);
}
virtual void InsertConcurrently(KeyHandle handle) override {
skip_list_.InsertConcurrently(static_cast(handle));
}
小结
MemTable是RockDB中非常重要的数据,用于在内存中维护插入的数据,并且一些重要的功能,如prefix seek,concurrent insert,都对它有依赖。
本篇主要简要介绍MemTable的基本功能和实现,更多的细节在分析其他功能时,还会回来更深入的了解。
参考资料:
https://github.com/facebook/rocksdb/wiki/MemTable