日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

Rocksdb DeleteRange实现原理

發布時間:2023/11/27 生活经验 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Rocksdb DeleteRange实现原理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

      • 1. 基本介紹
      • 2. 兩種接口使用及簡單性能對比
      • 3. DeleteRange 的基本實現
        • 3.1 寫流程的實現
        • 3.2 讀流程的實現 -- skyline算法

以下涉及到的代碼都是基于rocksdb 6.4.0版本進行描述的

1. 基本介紹

DeleteRange接口的設計是為了代替傳統的刪除一個區間[start,end) 內的key-value的接口

Slice start, end;
// set start and end
auto it = db->NewIterator(ReadOptions());for (it->Seek(start); cmp->Compare(it->key(), end) < 0; it->Next()) {db->Delete(WriteOptions(), it->key());
}

但是這樣的實現會有一些問題,它的刪除邏輯是先使用迭代器查找,再進行Delete(本質上還是寫,將一個Delete type的key寫入),而且這個過程也不是原子操作。所以,這樣的接口實現對與寫性能要求比較高的場景會嚴重降低系統性能。

同時刪除一段區間內的key 這樣的操在很多系統中都很常見,很多數據庫系統都會在多主鍵的基礎上構建其schema,這一些主鍵擁有相同的公共前綴,用來加速查找和壓縮存儲數據。所以刪除操作在存儲引擎這里就是刪除一段區間內的key。

像MyRocks作為一種MySQL的存儲引擎,其內部會用每一個key的前四個字節標識其所屬的表或者索引,當刪除一個表或者一個索引的時候,在存儲引擎這里就是刪除一段區間。

同時還有Cassandra的Rockssandra的存儲引擎,Marketplace使用的Rocksdb都是這樣的實現,那么DeleteRange接口的推出就是自然而然了,需要保證寫性能的同時不能降低讀性能。

Slice start, end;
// set start and end
db->DeleteRange(WriteOptions(), start, end);

2. 兩種接口使用及簡單性能對比

在介紹實現原理之前,我們可以先看看怎么使用,以及使用之后相比于傳統的刪除接口的性能差異有多少

基本接口使用

#include <iostream>
#include <string>
#include <rocksdb/db.h>
#include <rocksdb/iterator.h>
#include <rocksdb/table.h>
#include <rocksdb/options.h>
#include <rocksdb/env.h>
#include <ctime>using namespace std;//生成隨機key
static string rand_key(unsigned long long key_range) {char buff[30];unsigned long long n = 1;for (int i =1; i <= 4; ++i) {n *= (unsigned long long ) rand();}sprintf(buff, "%llu", n % key_range);string k(buff);return k;
}int main() {rocksdb::DB *db;rocksdb::Options option;option.create_if_missing = true;option.compression = rocksdb::CompressionType::kNoCompression;//創建dbrocksdb::Status s = rocksdb::DB::Open(option, "./iterator_db", &db);if (!s.ok()) {cout << "Open failed with " << s.ToString() << endl;exit(1);}rocksdb::DestroyDB("./iterator_db", option);//寫入for(int i = 0; i < 5; i ++) {rocksdb::Status s = db->Put(rocksdb::WriteOptions(), rand_key(9), string(10, 'a' + (i % 26)) );if (!s.ok()) {cout << "Put failed with " << s.ToString() << endl;exit(1);}}   //先遍歷一遍寫入的key-valuecout << "after put , seek all keys :" << endl;rocksdb::ReadOptions read_option;auto it=db->NewIterator(read_option);for (it->SeekToFirst(); it->Valid(); it->Next()) {cout << it->key().ToString() << " " << it->value().ToString() << endl;}//刪除從[2,5)之間的keyrocksdb::Slice start("2");rocksdb::Slice end("5");s = db->DeleteRange(rocksdb::WriteOptions(),db->DefaultColumnFamily(), start.ToString(), end.ToString());assert(s.ok());//遍歷刪除之后的key-valuecout << "after DeleteRange from 2-5 , seek all keys :" << endl;it = db->NewIterator(read_option);for (it->SeekToFirst(); it->Valid(); it->Next()) {cout << it->key().ToString() << " " << it->value().ToString() << endl;}db->Close();delete db;return 0;
}

輸出如下:

after put , seek all keys :
3 cccccccccc
4 dddddddddd
7 bbbbbbbbbb
8 eeeeeeeeee
after DeleteRange from 2-5 , seek all keys :
7 bbbbbbbbbb
8 eeeeeeeeee

我們接下來看看兩個接口的性能差異:

在代碼deleteRange 邏輯前后增加時間戳,打印一下消耗時間

    string start("1000");string end("5000");ts = clock();for(it ->Seek(start); it->Valid() && it->key().ToString() < end; it->Next()) {db->Delete(rocksdb::WriteOptions(), it->key());}//s = db->DeleteRange(rocksdb::WriteOptions(),db->DefaultColumnFamily(), start.ToString(), end.ToString());assert(s.ok());cout << "Old Delete Range use " << clock() - ts << endl;

兩者最后的時間差異對比如下:

DeleteRange use 30us
Old Delete Range use 193519us

當然我這個測試并不嚴謹,僅僅是刪除4000個key,且沒有讀寫混合。但是對照組只有這兩個接口,其他的環境,基本配置,寫入的數據量都是一樣的,且反復跑了多次,其實是能夠說明這個接口效率的提升的。

社區有更加嚴謹的benchmark測試

3. DeleteRange 的基本實現

3.1 寫流程的實現

之前我們描述Rocksdb 事務的基本實現中,有說過Rocksdb事務的寫實現是通過writebach的方式,同樣為了保證DeleteRange的一致性,會將其通過WriteBatch保存其range tombstone(即刪除的key的區間),然后按照writebatch的寫流程進行寫入,WriteBatch的寫入可以參考 圖3.1。

  • 為了保證讀性能,寫memtable的過程會為該range tombstone創建一個專門的range_del_table,使用skiplist來管理其中的數據,當讀請求下發時近需要從該range tombstone中索引對應的key,存在則直接返回Not Found
  • 寫入SST的時候,sst為其同樣預留了一段專門的存儲區域range tombstone block,這個block屬于元數據的block。也是為了在讀請求下發到sst的時候能夠從sst中的指定區域判斷key是否在deleterange 的范圍內部。
    參考 圖3.2
  • compaction或者flush的時候會清除掉過時的tombstone數據(當該sst攜帶的tombstone到達最LSM tree最底層的時候認為存儲的tombstone已經過時,此時會將其清除掉;或者當前的key之前的版本沒有snapshot引用,則同樣可以被清除)


圖3.1 writebatch 寫入


圖3.2 writebach寫入memtable和sst,為tombstone生成獨立的memtable

?

最終在SST中的存儲格式為一個單獨的range tombstone block,關于sst文件詳細格式可以看考sst文件詳細格式,這里借用官方的圖來描述


藍色區域數據MetaBlock,而range tombestone 作為其中的一個元數據區域進行存儲。

寫入tombestoe的具體代碼實現如下:

  • 通過DeleteRange接口,調用到DeleteRangeCF --> DeleteImpl --> mm->add ,通過memtable 的add函數將range tombstone加入自己的memtable(默認還是通過skiplit 實現的管理結構),關于rocksdb的跳表的實現可以參考inlineskiplist.h中的InlineSkipList<Comparator>::Insert函數。

    Memtable 的add函數內部實現如下:

    bool MemTable::Add(SequenceNumber s, ValueType type,const Slice& key, /* user key */const Slice& value, bool allow_concurrent,MemTablePostProcessInfo* post_process_info, void** hint) {......//根據傳入的valueType分配對應的memtable,這里就是為range delete分配屬于它的memtable//我們默認的memtable 的管理數據結構是跳表std::unique_ptr<MemTableRep>& table =type == kTypeRangeDeletion ? range_del_table_ : table_;KeyHandle handle = table->Allocate(encoded_len, &buf);......//通過在如下函數中調用跳表的insert函數將其插入到table之中(這個版本6.4.6默認開啟并發寫memtable)bool res = (hint == nullptr)? table->InsertKeyConcurrently(handle): table->InsertKeyWithHintConcurrently(handle, hint);if (UNLIKELY(!res)) {return res;}
    }
    
  • 寫入到memtable之后,會到DeleteImpl之中調用CheckMemtableFull函數,嘗試flush range tomstone的 memtable。此時也就進入range tombstone的第二階段,寫入sst文件,并參與compaction。

    我們直接進入到compaction真正進行計算并寫入到sst文件的核心函數ProcessKeyValueCompaction 之中,因為compaction的邏輯就是先從底層sst文件中讀入k-v數據,經過一系列的排序合并,最終將k-v數據再寫入到對應的sst文件之中。

    • 所以該函數針對range tombstone的處理就是一開始就需要先收集之前sst文件的range tombstone數據。通過構建通用的迭代器MakeInputIterator的過程中調用CompactionRangeDelAggregator::AddTombstones函數來完成compaction時訪問range tombstone的迭代器構建。

      void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {......CompactionRangeDelAggregator range_del_agg(&cfd->internal_comparator(),existing_snapshots_);// 通過迭代器,添加tombstones,構建好的key 底層迭代器就是一個最小堆,這個函數內部還會完成針對所有key的最小堆的構建。std::unique_ptr<InternalIterator> input(versions_->MakeInputIterator(sub_compact->compaction, &range_del_agg, env_options_for_read_));
      
    • 后續compaction的過程中,調用c_iter->SeekToFirst(); 以及c_iter->Next(),控制迭代器的移動。同時,內部實現會處理Range tombstone。他們都會調用同一個函數CompactionIterator::NextFromInput() ,當一個internal key處理完成之后需要從內部重新調整此時參與compation的key數據(遍歷的方式),能夠刪除的需要清除,能夠合并的需要合并。

      這里針對NextFromInput函數中處理 Range tombstone的部分主要有兩個地方

      1. 處理的key的type是merge的時候,需要將當前key的歷史seq都進行合并,合并的時候也會處理range tombstone
      2. 當key的type是 新的Put的時候,則同樣可以清除之前所有的tombstone

      第一個邏輯我們需要進入函數MergeUntil,根據合并后的結果調用range_del_agg->ShouldDelete函數,確認當前key是否能從range tombstone刪除。合并操作會將當前internal key對應的歷史版本進行合并,包括put/delete

      a. 如果這個時候當前key之前的版本沒有被快照引用,那么對于deleterange來說就可以刪除掉了。

      b. 如果當前key是put,且當前internal key的低版本key在tombstone中,那么低版本的key也能夠被tomestone跳過

      //MergeUntil  函數清理range_del_aggconst Slice val = iter->value();const Slice* val_ptr;if (kTypeValue == ikey.type &&(range_del_agg == nullptr ||!range_del_agg->ShouldDelete(ikey, RangeDelPositioningMode::kForwardTraversal))) {val_ptr = &val;} else {val_ptr = nullptr;}// 詳細的清理過程如下,默認是Forward的方式進行遍歷
      bool ForwardRangeDelIterator::ShouldDelete(const ParsedInternalKey& parsed) {// Move active iterators that end before parsed.//如果迭代器中已經保存的key比當前解析的key版本還低,即tombstone保存的key版本低。while (!active_iters_.empty() && icmp_->Compare((*active_iters_.top())->end_key(), parsed) <= 0) {TruncatedRangeDelIterator* iter = PopActiveIter();//從binary_heap維護的迭代器中移除頂部的元素,并重構內部的二分堆do {iter->Next();} while (iter->Valid() && icmp_->Compare(iter->end_key(), parsed) <= 0);PushIter(iter, parsed);assert(active_iters_.size() == active_seqnums_.size());}// Move inactive iterators that start before parsed.while (!inactive_iters_.empty() &&icmp_->Compare(inactive_iters_.top()->start_key(), parsed) <= 0) {TruncatedRangeDelIterator* iter = PopInactiveIter();while (iter->Valid() && icmp_->Compare(iter->end_key(), parsed) <= 0) {iter->Next();}PushIter(iter, parsed);assert(active_iters_.size() == active_seqnums_.size());}return active_seqnums_.empty()? false: (*active_seqnums_.begin())->seq() > parsed.sequence;
      }
      

      第二個邏輯則就比較簡單了,當前key是put的時候,可以直接將當前key之前所有的tombstone都清除掉

            // 1. new user key -OR-// 2. different snapshot stripebool should_delete = range_del_agg_->ShouldDelete(key_, RangeDelPositioningMode::kForwardTraversal);if (should_delete) {++iter_stats_.num_record_drop_hidden;++iter_stats_.num_record_drop_range_del;input_->Next();} else {valid_ = true;}
      
    • 接下來回到ProcessKeyValueCompaction邏輯中,c_iter->SeekToFirst(); 以及c_iter->Next()的邏輯是將能夠刪除的tombstone清理掉,實際上還有一些場景無法清理。

      比如:ikey1(internal key1) 是range delete,但是之前的版本中有snapshot引用,則此時無法清理掉該tombstone

      此時需要將該key寫入到tombstone對應的sst metadata 區域,進行固化

      // ProcessKeyValueCompaction 函數之中
      while (status.ok() && !cfd->IsDropped() && c_iter->Valid()) {// Invariant: c_iter.status() is guaranteed to be OK if c_iter->Valid()// returns true.const Slice& key = c_iter->key();const Slice& value = c_iter->value();......sub_compact->builder->Add(key, value); //內部寫入tombstone對應的builder之中
      

      當compaction中builder添加的key+value size大小超過了Max_output_size的時候則會觸發一次FinishCompactionOutputFile,通過這個函數進一步進行range tombstonde的固化邏輯,最終通過builder->Finish()函數寫入tombstonde的block之中

      // ProcessKeyValueCompaction函數之中
      if (sub_compact->compaction->output_level() != 0 &&sub_compact->current_output_file_size >=sub_compact->compaction->max_output_file_size()) {// (1) this key terminates the file. For historical reasons, the iterator// status before advancing will be given to FinishCompactionOutputFile().input_status = input->status();output_file_ended = true; //標記可以進行ouput到文件里了}if (output_file_ended) {const Slice* next_key = nullptr;if (c_iter->Valid()) {next_key = &c_iter->key();}CompactionIterationStats range_del_out_stats;//此時可以將累計的key+value固化到對應的sst結構中status =FinishCompactionOutputFile(input_status, sub_compact, &range_del_agg,&range_del_out_stats, next_key);}
      

      builder->Finish()函數實現:

      Status BlockBasedTableBuilder::Finish() {Rep* r = rep_;assert(r->state != Rep::State::kClosed);bool empty_data_block = r->data_block.empty();.......WriteRangeDelBlock(&meta_index_builder);
      }
      

最終,需要被清理的range tombstone被清理了。無法被清理的,則會被寫入到下一個sst文件中的tombstone block之中,等待之后的清理。

3.2 讀流程的實現 – skyline算法

Rocksdb的讀流程,拿到一個讀請求,如果是同一個事務內部的讀,則會先從該事務對應的writebatch中讀;如果是非事務,則讀的順序是memtable,immutable memtable ,table cache,SSTs。

如之前我們通過迭代器訪問db中的數據時,本身邏輯就是完整的讀流程。而且實際的生產環境中,通過迭代器進行訪問居多,因為迭代器提供了不同方式的訪問邏輯。

為了提升讀性能,快速定位到一個key是否在range tombstone區間之中,這里針對迭代器進行了優化。在memtable,immu,table cache, sst的 range tombstone的路徑之上構造一個skyline,skyline能夠提供包含所有路徑中的tombstone的全集,而且是有序的,這樣只需要通過高效的二分查找來確定一個key是否在range tombstone之間。構建過程如 圖3.2.1


圖3.2.1 構建skyline,加速讀性能。橫軸代表的是key,縱軸代表該key對應的seqnum。其中A區域代表構建skyline之前,range tombstone存放在不同的區域,且其中可能有重疊的部分。構建完成skyline之后就變成了圖B的樣子,能夠提供二分查找,減少了在不同區域的重復查找問題。

這里skyline的實現是參考leetcode的一個算法實現 218天際線問題

總結

以上是生活随笔為你收集整理的Rocksdb DeleteRange实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。