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

歡迎訪問 生活随笔!

生活随笔

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

生活经验

存储引擎 K/V 分离下的index回写问题

發布時間:2023/11/27 生活经验 33 豆豆
生活随笔 收集整理的這篇文章主要介紹了 存储引擎 K/V 分离下的index回写问题 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前言

近期在做on nvme hash引擎相關的事情,對于非全序的數據集的存儲需求,相比于我們傳統的LSM或者B-tree的數據結構來說 能夠減少很多維護全序上的計算/存儲資源。當然我們要保證hash場景下的高寫入能力,append-only 還是比較友好的選擇。

像 Riak的bitcask 基于索引都在內存的hash引擎這種,在后期的針對data-file 的merge 完成之后會將新的key-value-index回填到內存中的hash索引中,這個過程在實際的生產環境對性能有多大的影響還不太好確定。但是,很明顯的一點是正確的hash引擎索引在高并發場景中的更新是需要加鎖的。而一旦有了排他鎖,也就意味著CPU的獨占,這個時候用戶的讀取和插入 就會和merge 之后的index回填發生鎖的競爭,從而影響引擎的外部性能。

而同樣的問題在以 Wisckey 為首的 LSM-tree key-value 分離策略中尤為明顯,包括Titan, rocksdb的BlobDB,BadgerDB 都會面臨這樣的問題,他們在compaction 之后的回填 大value-index 還需要產生I/O操作,這個代價可能會更高,那他們是怎么解決這個問題的呢?

探索他們的解決辦法不一定完全能夠借鑒到hash 引擎的實現中,不過是可以提供一個解決思路。

Titan 的回寫策略

關于Titan的 GC 策略介紹可以參考:Titan GC策略實現
Titan 是 pingcap 早期基于wisckey 做出來的key-value 分離存儲引擎,可以作為rocksdb 的一個插件來使用。
它的解決辦法是提供一個可配置項gc_merge_rewrite

  1. 關閉:會在GC 過程中將key-value寫入新的blobfile 之后,通過正常的Write with Callback + Put 接口回寫blob-index到lsm-tree。這個也就是默認回寫的方式,Titan的Callback 是一個Get操作,在寫入之前會先嘗試讀一下這個key 是否在lsm-tree中,如果不在就不會寫入了。而且會將新的key + key-index 完全寫入。
  2. 開啟:則是一個回寫產生的性能問題和讀性能之間的一個trade-off。開啟之后直接寫入 一個kMergeType blob-index,這種情況下不需要去執行Callback了,而是直接寫入Merge操作,后續通過compaction 進行 key的blob-index的合并 或者 讀請求命中這個key的時候會進行merge。merge請求本省不會攜帶原本大小的value,所以不會產生較大的寫放大,只是在讀的時候需要將當前key之前的merge都進行合并,對讀性能可能有較大的影響。

相關的實現代碼可以參考:

void BlobGCJob::BatchWriteNewIndices(BlobFileBuilder::OutContexts& contexts,Status* s) {...// 關閉merge,調用默認的寫入方式if (!gc_merge_rewrite_) {merge_blob_index.EncodeToBase(&index_entry);// Store WriteBatch for rewriting new Key-Index pairs to LSM// 在這個策略下,rewrite_batches_ 最后的消費是通過 Rocksdb::WriteWithCallback實現// 的,在寫入的時候會執行 Callback,里面會去查一下key是否存在。GarbageCollectionWriteCallback callback(cfh, ikey.user_key.ToString(),std::move(original_index));callback.value = index_entry;rewrite_batches_.emplace_back(std::make_pair(WriteBatch(), std::move(callback)));auto& wb = rewrite_batches_.back().first;*s = WriteBatchInternal::PutBlobIndex(&wb, cfh->GetID(), ikey.user_key,index_entry);} else { // 開啟,rewrite_batches_without_callback_ 的消費過程是 直接寫入Merge 類型的keymerge_blob_index.EncodeTo(&index_entry);rewrite_batches_without_callback_.emplace_back(std::make_pair(WriteBatch(), original_index.blob_handle.size));auto& wb = rewrite_batches_without_callback_.back().first;*s = WriteBatchInternal::Merge(&wb, cfh->GetID(), ikey.user_key,index_entry);}...
}

最終對兩個數據結構的消費邏輯統一是在RewriteValidKeyToLSM函數中。

BlobDB 的回寫策略

BlobDB 的大體特性可以參考BlobDB 特性及性能測試結果。
因為BlobDB 新版本是社區比較推薦的一個k/v分離的穩定版本,基本的Rocksdb特性都已經支持了,包括trasaction/checkpoint/backup 等這一些不常用但很重要的功能都已經支持了。除了像merge/ingest等更為偏的能力暫時還不支持。

BlobDB的在GC上的一個考慮就不想因為后續頻繁的回寫處理影響正常的請求。
如果開啟了GC enable_blob_garbage_collection

  1. 則在compaction過程中,迭代器 處理 類型 kTypeBlobIndex 的key時會進入到GarbageCollectBlobIfNeeded,因為分離存儲的時候lsm中存放的value 是key-index,即這個value能夠索引的到blobfile的一個index。
  2. 確認當前blob能夠參與GC 且 當前key需要被保留,則根據key-index 讀取到blob_value 并 直接寫入到新的blob-file中。并且將新的blob-index 作為當前key的value,提取出來。
  3. key 和 新的key-index 繼續參與compaction后續的落盤行為。

主體第二步,也就是想要GC的話會在compaction過程中直接將過期的blob-value直接回收,compaction完成之后 lsm的sst 以及 blob都會被更新到,只需要維護后續的舊的blob回收即可。

代碼實現如下:

  1. compaciton過程中(迭代器按key處理階段) 調度GC
    void CompactionIterator::PrepareOutput() {if (valid_) {if (ikey_.type == kTypeValue) {ExtractLargeValueIfNeeded();} else if (ikey_.type == kTypeBlobIndex) {// 調度GCGarbageCollectBlobIfNeeded();}...
    }
    
  2. 按照上面的步驟進行處理:
    void CompactionIterator::GarbageCollectBlobIfNeeded() {...// 開啟GCif (compaction_->enable_blob_garbage_collection()) {BlobIndex blob_index;{// 1. 獲取blobindexconst Status s = blob_index.DecodeFrom(value_);if (!s.ok()) {status_ = s;valid_ = falsereturn;}}if (blob_index.IsInlined() || blob_index.HasTTL()) {status_ = Status::Corruption("Unexpected TTL/inlined blob index");valid_ = false;return;}// 2. 確認當前blob-index 允許參與GCif (blob_index.file_number() >=blob_garbage_collection_cutoff_file_number_) {return;}const Version* const version = compaction_->input_version();assert(version);{// 3. 解析讀出來當前blob數據const Status s =version->GetBlob(ReadOptions(), user_key(), blob_index, &blob_value_);if (!s.ok()) {status_ = s;valid_ = false;return;}}value_ = blob_value_;// 4. 將讀出來的blob數據寫入到新的blob file,并構造新的 value-index 作為當前lsm-tree// 即將存儲的key的value.if (ExtractLargeValueIfNeededImpl()) {return;}ikey_.type = kTypeValue;current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);return;}...
    }
    

問題1: compaction過程中讀取大value和我們rocksdb 未k/v分離 場景下的讀取有什么區別?

這里的讀取只會是保留的key的real value,對于那一些要清理的key,則不會讀取。為了避免業務峰值觸發大量的compaciton以及 GC的讀取,GC的觸發可以通過SetOption 來動態調整。

問題2: 相比于 Titan GC 調度的優劣?

個人覺得,BlobDB的GC調度更為簡潔高效低成本。
來,我們對比一下GC過程中產生I/O的步驟:

  1. TitanDB,通過EventListener 在compaction過程中拿到需要參與GC 的blobfile 集合,compaction完成之后 對待GC的 blobfile 進行iter 迭代。
    a. 拿到每一個key 去 LSM 點查 是否存在。
    b. 存在,則讀取其所在blobfile 的 大value,寫入到新的blobfile
    c. 寫入key 以及 新的value-index 到 LSM -tree(伴隨著后續的逐層compaction,或者 merge的合并)。
  2. BlobDB,直接在compaction過程中一起調度GC。
    a. 不需要反查,compaction過程中知道這個key是要keep還是要skip,直接對keep下來的key 讀取blobfile的大value,寫入到新的blobfile.
    b. 繼續compaction時直接將當前要keep下來的key 以及 新的 value-index 寫入 lsm即可。

可以看到,blobdb 的第二個步驟是正常的compaction寫入邏輯,相比于Titan來說,其實也就只進行了 Titan有效的第二步,少了第一步的點查和第三步的回寫。除此之外,Rocksdb的可調性更高一些,可以針對必要的GC時的大value讀寫進行控制,允許動態調整,從而最大程度得減少了GC對上層請求的性能影響。

具體在 GC 過程中的性能差異會在后續補充上。

BadgerDB 的回寫策略

Badger 作為 dGraph 社區備受 cgo 折磨之后推出的自研k/v 分離存儲引擎,在go 語言中還是非常受歡迎的。
本文僅討論BadgerDB 在k/v 分離場景的回寫策略,對于其測試優于Rocksdb(rocksdb的默認參數) 以及 其相比于Rocksdb 的其他優秀設計暫不展開討論。

Badger的大value是存放在value log文件中,它很聰明的一點是GC 接口只交給用戶來調度,而不是自己內部自主觸發,這樣的責任劃分就非常清晰了,用戶自己選擇開啟關閉GC,來自己承擔GC引入的讀寫問題,真是機智。
當然BadgerDB 這里的GC回寫并沒有看到太亮眼的設計,就是在對 value log 進行GC的時候和Titan不開啟gc_merge_rewrite 邏輯差不多。

  1. 選擇好了待GC的value-log文件,先從lsm中嘗試讀取key,存在則需要將value寫入到新的value log中。
  2. 完成寫入新的value-log之后,會將最終的key, value-index 更新到lsm-tree中。

回寫源代碼基本在RunValueLogGC 函數中的rewrite處理邏輯中,感興趣的可以看一下。

總結

可以看到為了解決在LSM-tree中大value 不隨著compaction一起調度而造成的性能問題,大家可謂是煞費苦心。Titan 嘗試做了一些優化,但整體來看還是不盡人意。Rocksdb 的 Blobdb 還是更加成熟,可以說是考慮得很全面了,從實現上看確實有很明顯的效果。而BadgerDB的做法更為徹底,這個問題我們不管,交給用戶自由調度,因為用戶大多數情況還是知道自己的業務什么時候處于高峰,什么時候處于低谷,產生的I/O競爭問題那是你們自己調度造成的,自己解決哈,🐂。

而回到最初的我們 hash-engine 的 hash-index回寫問題,其實可以考慮借鑒一下 BlobDB的做法,不過需要接口做的更靈活一些。

總結

以上是生活随笔為你收集整理的存储引擎 K/V 分离下的index回写问题的全部內容,希望文章能夠幫你解決所遇到的問題。

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