Prefix Seek
Prefix seek是RocksDB的一種模式,主要影響Iterator的行為。
在這種模式下,RocksDB的Iterator并不保證所有key是有序的,而只保證具有相同前綴的key是有序的。這樣可以保證具有相同特征的key(例如具有相同前綴的key)盡量地被聚合在一起。
使用方法
prefix seek模式的使用如下,
int main() {
DB* db;
Options options;
options.IncreaseParallelism();
options.OptimizeLevelStyleCompaction();
options.create_if_missing = true;
options.prefix_extractor.reset(rocksdb::NewFixedPrefixTransform(3));
Status s = DB::Open(options, kDBPath, &db);
assert(s.ok());
s = db->Put(WriteOptions(), "key1", "value1");
assert(s.ok());
s = db->Put(WriteOptions(), "key2", "value2");
assert(s.ok());
s = db->Put(WriteOptions(), "key3", "value3");
assert(s.ok());
s = db->Put(WriteOptions(), "key4", "value4");
assert(s.ok());
s = db->Put(WriteOptions(), "otherPrefix1", "otherValue1");
assert(s.ok());
s = db->Put(WriteOptions(), "abc", "abcvalue1");
assert(s.ok());
auto iter = db->NewIterator(ReadOptions());
for (iter->Seek("key2"); iter->Valid(); iter->Next())
{
std::cout << iter->key().ToString() << ": " << iter->value().ToString() << std::endl;
}
delete db;
return 0;
}
輸出如下
key2: value2
key3: value3
key4: value4
otherPrefix1: otherValue1
從上面的輸出,我們可以看到prefix seek模式下iterator的幾個(gè)特性
- 首先seek(targetKey)方法會(huì)將iterator定位到具有相同前綴的區(qū)間,并且大于或等于targetKey的位置.
- Next()方法是會(huì)跨prefix的,就像例子中,當(dāng)以"key"為前綴的key都遍歷完之后,跨到了下一個(gè)prefix "oth"上繼續(xù)遍歷,直到遍歷完所有的key,Valid()方法返回false.
- 在遍歷的時(shí)候,如果只想遍歷相同前綴的key,需要在每一次Next之后,判斷一次key是前綴是否符合預(yù)期.
auto iter = db->NewIterator(ReadOptions());
Slice prefix = options.prefix_extractor->Transform("key0");
for (iter->Seek("key0"); iter->Valid() && iter->key().starts_with(prefix); iter->Next())
{
std::cout << iter->key().ToString() << ": " << iter->value().ToString() << std::endl;
}
輸出如下:
key1: value1
key2: value2
key3: value3
key4: value4
Prefix Seek相關(guān)的一些優(yōu)化
為了更快地定位指定prefix的key,或者排除不存在的key,RocksDB做了一些優(yōu)化,包括:
- prefix bloom filter for block based table
- prefix bloom for memtable
- memtable底層使用hash skiplist
- 使用plain table
一個(gè)典型的優(yōu)化設(shè)置見下例
Options options;
// Enable prefix bloom for mem tables
options.prefix_extractor.reset(NewFixedPrefixTransform(3));
options.memtable_prefix_bloom_bits = 100000000;
options.memtable_prefix_bloom_probes = 6;
// Enable prefix bloom for SST files
BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DB* db;
Status s = DB::Open(options, "/tmp/rocksdb", &db);
......
auto iter = db->NewIterator(ReadOptions());
iter->Seek("foobar"); // Seek inside prefix "foo"
Prefix Seek相關(guān)類圖
說(shuō)是Prefix Seek相關(guān)的類圖,其實(shí)就是rocksdb的Iterator類圖。這里列出了幾個(gè)主要的類。
- InternalIterator
接口類,定義了iterator的接口,包括Seek、Next、Prev等接口。 - MergingIterator
返回給用戶的實(shí)際Iterator類型,之所以叫"Merging",是因?yàn)樗钟衜emtable和sst文件的iterater,對(duì)外提供了統(tǒng)一的iterator接口。 - MemTableIterator
用于遍歷memtable的iterator。主要的分析對(duì)象。 - TwoLevelIterator
用于遍歷sst文件的iterator。
Prefix Seek工作流程
Iterator簡(jiǎn)單分析
通過(guò)Iterator的相關(guān)接口,來(lái)逐層分析,持有PrefixTransform對(duì)象的option.prefix_extractor選項(xiàng)是如何工作的。
DB的Iterator由三部分組成
- 當(dāng)前memtable的iterator
- 所有不可修改的memtable的iterator
- 所有l(wèi)evel上的sst文件的iterator(TwoLevelIterator)
這些iterator統(tǒng)一由MergeIterator來(lái)管理。MergeIterator持有一個(gè)vector成員,上面三類iterator都會(huì)添加到該成員中。
autovector<IteratorWrapper, kNumIterReserve> children_;
autovector是對(duì)std::vector的完全模板特化版本
class autovector : public std::vector<T> {
using std::vector<T>::vector;
};
autovector主要針對(duì)在棧上分配的數(shù)組,保存數(shù)量較少的item這種使用場(chǎng)景做了優(yōu)化。
IteratorWrapper是對(duì)InternalIterator*類型的對(duì)象的一個(gè)簡(jiǎn)單封裝,主要cache了iter)->Valid()和iter_->key()的結(jié)果,提供更好的局部性訪問(wèn)性能。其他接口和InternalIterator一樣,只是轉(zhuǎn)發(fā)請(qǐng)求給iter_。
DB的Iterator添加當(dāng)前使用的memtable的iterator的代碼如下
InternalIterator* DBImpl::NewInternalIterator(
const ReadOptions& read_options, ColumnFamilyData* cfd,
SuperVersion* super_version, Arena* arena,
RangeDelAggregator* range_del_agg) {
...
merge_iter_builder.AddIterator(
super_version->mem->NewIterator(read_options, arena));
...
arena是為所有iterator已經(jīng)分配的一塊內(nèi)存。
向MergeIterator添加Iterator的實(shí)現(xiàn):
- 先將要添加的iter加入到vector成員children_中
- 如果iter是有效的iterator,將該iter添加到一個(gè)最小堆成員中。
virtual void AddIterator(InternalIterator* iter) {
assert(direction_ == kForward);
children_.emplace_back(iter);
if (pinned_iters_mgr_) {
iter->SetPinnedItersMgr(pinned_iters_mgr_);
}
auto new_wrapper = children_.back();
if (new_wrapper.Valid()) {
minHeap_.push(&new_wrapper);
current_ = CurrentForward();
}
}
這里的最小堆minHeap_用于維護(hù)上面所有合法iterator的訪問(wèn)順序,指向key較小的iter,越靠近堆頂,這樣,當(dāng)連續(xù)多次調(diào)用Next接口時(shí),都在同一個(gè)child iterator上,可以直接通過(guò)堆頂?shù)膇terator來(lái)獲取Next,而不用遍歷所有children。
如果堆頂iterator的下一個(gè)元素的不是合法的,則將該iterator從堆中pop出去,調(diào)整堆之后,拿到新的堆頂元素。
DB的Iterator MergingIterator的Next接口主要實(shí)現(xiàn)如下
virtual void Next() override {
assert(Valid());
...
// For the heap modifications below to be correct, current_ must be the
// current top of the heap.
assert(current_ == CurrentForward());
// as the current points to the current record. move the iterator forward.
current_->Next();
if (current_->Valid()) {
// current is still valid after the Next() call above. Call
// replace_top() to restore the heap property. When the same child
// iterator yields a sequence of keys, this is cheap.
minHeap_.replace_top(current_);
} else {
// current stopped being valid, remove it from the heap.
minHeap_.pop();
}
current_ = CurrentForward();
Seek接口的實(shí)現(xiàn)
Seek一個(gè)target的實(shí)現(xiàn),通過(guò)遍歷每個(gè)child iterator,然后將合法的iterator插入到最小堆minHeap中,將current_成員指向minHeap的堆頂。
virtual void Seek(const Slice& target) override {
ClearHeaps();
for (auto& child : children_) {
{
PERF_TIMER_GUARD(seek_child_seek_time);
child.Seek(target);
}
PERF_COUNTER_ADD(seek_child_seek_count, 1);
if (child.Valid()) {
PERF_TIMER_GUARD(seek_min_heap_time);
minHeap_.push(&child);
}
}
direction_ = kForward;
{
PERF_TIMER_GUARD(seek_min_heap_time);
current_ = CurrentForward();
}
}
以當(dāng)前可寫的memtable的iterator為例,當(dāng)用戶調(diào)用Seek接口時(shí),如何定位到memtable中的目標(biāo)key。
MemTableIterator的Seek
在memtable中持有一個(gè)MemTableRep::Iterator*類型的iterator指針iter_,iter_指向的對(duì)象的實(shí)際類型由MemTableRep的子類中分別定義。以SkipListRep為例,
在MemTableIterator的構(gòu)造函數(shù)中,當(dāng)設(shè)置了prefix_seek模式時(shí),調(diào)用MemTableRep的GetDynamicPrefixIterator接口來(lái)獲取具體的iterator對(duì)象。
MemTableIterator(const MemTable& mem, const ReadOptions& read_options,
Arena* arena, bool use_range_del_table = false)
: bloom_(nullptr),
prefix_extractor_(mem.prefix_extractor_),
comparator_(mem.comparator_),
valid_(false),
arena_mode_(arena != nullptr),
value_pinned_(!mem.GetMemTableOptions()->inplace_update_support) {
if (use_range_del_table) {
iter_ = mem.range_del_table_->GetIterator(arena);
} else if (prefix_extractor_ != nullptr && !read_options.total_order_seek) {
bloom_ = mem.prefix_bloom_.get();
iter_ = mem.table_->GetDynamicPrefixIterator(arena);
} else {
iter_ = mem.table_->GetIterator(arena);
}
}
對(duì)于SkipList來(lái)說(shuō),GetDynamicPrefixIterator
也是調(diào)用GetIterator,只有對(duì)HashLinkListRep和HashSkipListRe,這個(gè)方法才有不同的實(shí)現(xiàn)。
virtual MemTableRep::Iterator* GetIterator(Arena* arena = nullptr) override {
if (lookahead_ > 0) {
void *mem =
arena ? arena->AllocateAligned(sizeof(SkipListRep::LookaheadIterator))
: operator new(sizeof(SkipListRep::LookaheadIterator));
return new (mem) SkipListRep::LookaheadIterator(*this);
} else {
void *mem =
arena ? arena->AllocateAligned(sizeof(SkipListRep::Iterator))
: operator new(sizeof(SkipListRep::Iterator));
return new (mem) SkipListRep::Iterator(&skip_list_);
}
}
這樣就創(chuàng)建了MemTableIterator。
當(dāng)調(diào)用到MemTableIterator的Seek
接口時(shí):
- 首先在DynamicBloom成員bloom_中查找目標(biāo)key的prefix是否可能在當(dāng)前的memtable中存在。
- 如果不存在,則一定不存在,可以直接返回。
- 如果可能存在,則調(diào)用MemTableRep的iterator進(jìn)行Seek。
virtual void Seek(const Slice& k) override {
PERF_TIMER_GUARD(seek_on_memtable_time);
PERF_COUNTER_ADD(seek_on_memtable_count, 1);
if (bloom_ != nullptr) {
if (!bloom_->MayContain(
prefix_extractor_->Transform(ExtractUserKey(k)))) {
PERF_COUNTER_ADD(bloom_memtable_miss_count, 1);
valid_ = false;
return;
} else {
PERF_COUNTER_ADD(bloom_memtable_hit_count, 1);
}
}
iter_->Seek(k, nullptr);
valid_ = iter_->Valid();
}
當(dāng)調(diào)用MemTableRep的iterator進(jìn)行Seek時(shí),則是通過(guò)調(diào)用實(shí)際使用的數(shù)據(jù)結(jié)構(gòu)的Seek接口,找到第一個(gè)大于或等于key的目標(biāo)。
// Advance to the first entry with a key >= target
virtual void Seek(const Slice& user_key, const char* memtable_key)
override {
if (memtable_key != nullptr) {
iter_.Seek(memtable_key);
} else {
iter_.Seek(EncodeKey(&tmp_, user_key));
}
}
第一個(gè)分支主要用于確定在memtable中的key,做update類的操作.
MemTable Prefix Bloom的構(gòu)建
prefix bloom作為memtable的一個(gè)成員,當(dāng)向memtable插入一個(gè)key value時(shí),會(huì)截取key的prefix,插入到prefix bloom中。
void MemTable::Add(SequenceNumber s, ValueType type,
const Slice& key, /* user key */
const Slice& value, bool allow_concurrent,
MemTablePostProcessInfo* post_process_info) {
...
if (!allow_concurrent) {
...
if (prefix_bloom_) {
assert(prefix_extractor_);
prefix_bloom_->Add(prefix_extractor_->Transform(key));
}
...
else {
...
if (prefix_bloom_) {
assert(prefix_extractor_);
prefix_bloom_->AddConcurrently(prefix_extractor_->Transform(key));
}
...
}
...
}
關(guān)于bloom filter的原理和實(shí)現(xiàn)可以參考前文。傳送門
因?yàn)樵貰lockBasedTable中,對(duì)bloom filter的用法不同,所以使用了不同實(shí)現(xiàn)的bloom filter。基本原理是相同的。
SST文件的Iterator和Seek
比較復(fù)雜,后續(xù)再繼續(xù)分析。。
小結(jié)
這篇主要介紹rocksdb的prefix seek模式,也不可避免地分了很多iterator相關(guān)的東西,因?yàn)閜refix seek模式和iterator的關(guān)系非常緊密。由于篇幅原因,只介紹了memtable的iterator和seek。
參考資料:
https://github.com/facebook/rocksdb/wiki/Prefix-Seek-API-Changes