RocksDB. Prefix Seek源碼分析

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è)特性

  1. 首先seek(targetKey)方法會(huì)將iterator定位到具有相同前綴的區(qū)間,并且大于或等于targetKey的位置.
  2. Next()方法是會(huì)跨prefix的,就像例子中,當(dāng)以"key"為前綴的key都遍歷完之后,跨到了下一個(gè)prefix "oth"上繼續(xù)遍歷,直到遍歷完所有的key,Valid()方法返回false.
  3. 在遍歷的時(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)化,包括:

  1. prefix bloom filter for block based table
  2. prefix bloom for memtable
  3. memtable底層使用hash skiplist
  4. 使用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)類圖

prefix seek相關(guān)的iterator類圖

說(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由三部分組成

  1. 當(dāng)前memtable的iterator
  2. 所有不可修改的memtable的iterator
  3. 所有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):

  1. 先將要添加的iter加入到vector成員children_中
  2. 如果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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評(píng)論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,957評(píng)論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,081評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,485評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評(píng)論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,720評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,263評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,025評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,204評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,461評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評(píng)論 1 289
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,945評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,205評(píng)論 2 375

推薦閱讀更多精彩內(nèi)容

  • 布隆過(guò)濾器 Bloom Filter 布隆過(guò)濾器,用來(lái)判斷一個(gè)元素是否在集合中。它的特點(diǎn)是節(jié)省空間,但是有誤判。有...
    周肅閱讀 4,649評(píng)論 0 5
  • 背景 一年多以前我在知乎上答了有關(guān)LeetCode的問(wèn)題, 分享了一些自己做題目的經(jīng)驗(yàn)。 張土汪:刷leetcod...
    土汪閱讀 12,763評(píng)論 0 33
  • 通過(guò)之前對(duì)LevelDB的整體流程,數(shù)據(jù)存儲(chǔ)以及元信息管理的介紹,我們已經(jīng)基本完整的了解了LevelDB。接下來(lái)兩...
    CatKang閱讀 3,720評(píng)論 0 5
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,806評(píng)論 18 139
  • java筆記第一天 == 和 equals ==比較的比較的是兩個(gè)變量的值是否相等,對(duì)于引用型變量表示的是兩個(gè)變量...
    jmychou閱讀 1,511評(píng)論 0 3