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的部分主要有兩個地方
- 處理的key的type是merge的時候,需要將當前key的歷史seq都進行合并,合并的時候也會處理range tombstone
- 當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实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 买一个服务器多少钱?
- 下一篇: Mac 上使用 Clion 阅读C++源