LSM 优化系列(六)-- 【ATC‘20】MatrixKV : NVM 的PMEM 在 LSM-tree的write stall和写放大上的优化
文章目錄
- LSM 問題背景
- MatrixKV 設計細節
- 整體架構介紹
- Matrix Container介紹
- Receiver
- RowTable
- Compactor
- Space management
- Column Compaction介紹
- 對于Column Compaction的總結
- 讀加速 Cross-row Hint Search
- MatrixKv 寫入完整流程
- MatrixKV 讀取完整流程
- MatrixKV 性能
- 總結
這篇論文大家可能不了解,但是"華為天才少女 年薪150w" 那個熱搜女孩大家應該聽過。這里分享的這一篇論文是ATC’20 存儲技術相關的頂會今年收錄的一篇,她是一作;去年她的一篇GearDB: A GC-free Key-Value Store on HM-SMR Drives with Gear Compaction 被FAST’19 收錄,她也是一作。
她之前的相關論文并沒有再搜,但從這兩篇頂會以一作的身份來看,她本身的實力不言而喻。存儲技術的頂會大家可以看看相關的官網數據,國內被錄入的基本都是頂級互聯網公司級的團隊產出 以及 知名教授帶領的C9 top學校團隊。她以一作 貢獻最大的身份被錄入,這樣的年薪是實至名歸的(國內還是需要留住優秀人才的)。
當然,她所做的技術是數據庫/存儲相關的,同樣是TOP級公司極為看重的核心技術,也才會有這樣的優質待遇。
回到今天要討論的論文 : MatrixKV:Reducing Write Stalls and Write Amplification 上。
ps:本篇并非論文翻譯,下文的組織形態是比較簡化的,感覺可能會有信息缺失的同學可以直接看論文。
該論文是基于 rocksdb 5.18.3 版本實現的,源代碼MatrixKV-github
關于rocksdb compaction的一些基礎知識可以參考這兩篇。這是詳解,會涉及到源碼層級的分析。
1. SST文件詳細格式源碼解析
2. Compaction 完整實現過程 概覽
LSM 問題背景
本篇論文關注的問題背景是LSM-tree 帶來的 write-stall 以及 寫放大問題。
都是針對LSM的老生常談的問題,關于write-stall 直接參考SILK- Preventing Latency Spikes in Log-Structured Merge Key-Value Stores 中的Latency Spike 在LSM-tree中的體現即可。
其中Write-Stall的主體原因還是I/O資源的競爭,Higher-level compaction 與更高優先級的Flush和L0->L1 compaction的I/O資源進行競爭,導致更高優先級的internal 操作無法及時完成,最終體現在客戶端的操作就是Write-stall或者高長尾延時。 而造成write-stall 的主體compaction就是 L0->L1 的compaction過程,這個過程L0重復的key最多,但卻只有一個compaction 線程來做(傳統LSM 在更高層中 sst文件之間以及之內不允許又重疊key),所以效率也很低,這就在大壓力的場景下很大概率造成write-stall。
寫放大問題簡要概述一下,在PebblesDB Building Key-Value Stores using FLSM-Tree(Fragmented) 中的背景描述也有說。
如下圖
L1->L2 compaction的過程中,選擇一部分L1的sst文件,一部分L2的sst文件,compaction之后又寫入到了L2;這樣下一次 又調用的 L1->L2 的compaction 可能又會將之前的寫入的sst文件的key-value讀出來,重新合并排序,再次寫入到L2。這樣,很多key-value不斷的被讀寫,而自己本身并沒有發生變化。隨著LSM 層數的增加,讀寫放大的比例會越來越大 WAM = AF *n(wam 是寫放大的倍數,AF是寫放大的系數,n是LSM的層數)。
這就是在Level compaction過程中出現的讀寫放大,帶寬資源有限的情況下用戶態吞吐會被嚴重限制。
MatrixKV 的出現背景 就是想要在Write stall 和 寫放大上 進行一些優化,重心需要放在L0->L1的compaction速度慢問題之上。直接辦法是變更存儲介質(NVM-PMEM),只變更存儲介質,仍然會有write-stall的問題,畢竟L0->L1 compaction速度提升不上來。這里我比較好奇的是論文中并沒有提到subcompaction機制,rocksdb的subcompaction機制本身也是在L0->L1 compaction速度慢的情況下按照sst文件粒度拆分成多個compaction線程并發來做。
總之,只變更存儲介質為更高性能的(NVM-PMEM)是不夠的,論文中有Novel-LSM 的數據可以看到還是有大量write-stall。所以還需要變更L0->L1的數據結構,實現算法層的加速。至于,降低寫放大,通過這個公式 WAM = AF *n,論文中直接將 Level層數減少,比如原來的6層,減少為4層,并增大每一層的容量(這個優化略顯尷尬😅)。
NVM-PMEM性能
延時和吞吐 和內存DRAM處于一個量級,注意,是一個量級,實際還會差幾倍。但是相比于NAND ssd 延時好2個量級,帶寬也是有數倍的提升。
還有一種是DIMM-PMEM 持久內存,兩者的主要差異是I/O傳輸使用的系統總線不同,NVME是走PCIe總線,DIMM 是內存總線,所以DIMM 性能相比于PCIe的性能好一些。后續我的博客會介紹一些SATA,SAS,PCIe,NVMe 這一些之間的區別。
問題背景和優化方向已經做了一個總的描述,優化的結果 是L0-L1 compaction過程中 如何利用好NVM-PMEM做好算法的設計,也就是MatrixKV的 設計核心MatrixComtainer。
MatrixKV 設計細節
整體架構介紹
MatrixKV 在LSM 整體設計中 用了組合存儲,即DRAM,NVM-PMEM,SSD。
這里需要補充一點,減少Write-Stall 方法 在上文中的表述可以歸納為兩方面:
- 加速L0->L1 compaction的速度,也就是單位時間內完成compacion的數據總量越大越好
- 減少L0->L1 compaction 與 Higher Level compaction的資源競爭問題
所以MatrixKV 除了設計了全新的L0->L1 compaction算法,還將L0->L1的compaction和Higher Level compactions 分布在不同的存儲介質上,把Higher Level compactions 單獨放在SSD 上, 這樣就不會有I/O資源的競爭問題了。
整體MatrixKV的設計架構如下圖
架構圖中的基本組件及其作用如下:
- DRAM: 繼續保存mem/imm
- NVM-PMEM: MatrixContainer 來保存L0的key-value數據存儲 以及 L0->L1的compaction調度
- SSD:保存更高層的SST文件 以及 按照rocksdb原生compaction邏輯調度compaction
寫入流程如下:
- rocksdb原生邏輯,先更新DRAM中的memtable, memtable寫滿之后切換為immutable memtable
- imm flush到處于 NVM-PMEM中的matrix containter,又Receiver 組件負責接受flush的k-v數據,并通過pmdk寫入到NVM-PMEM之上
- receiver 切換為compactor 調度 column compaction
- column compaction結果最終會形成一個個SST文件落在SSD上的L1層,由后續compaction在更高層按照rocksdb的原生邏輯調度。
接下來詳細看一下Matrix Container組件及其設計細節。
Matrix Container介紹
Matrix container 可以看作是一個新的數據管理結構。
主要有兩個協調組件和一個k-v數據存儲結構 以及 NVM-PMEM空間管理結構:
- Receiver,負責接受imm的數據,轉化為一個RowTable 存儲,并為這個RowTable分配一個唯一標識的遞增編號
- Compactor,當Receiver存儲的RowTable總大小達到了Matrix Container容量的閾值(比如60%),切換為Compactor,將多個RowTable 按列并結合L1的sst文件 觸發Column Compaction。
- RowTable ,行表。每一行存儲一個imm,使用的是pmdk 的 底層pmem相關接口寫入到持久內存中。
- space-management ,為Receiver分配以page為粒度的存儲空間,回收compaction完成compaction之后的空間。
Receiver
Receiver 主要用作保存來自memtable的數據,準確的說是immtable中的數據,這一個過程是imm從DRAM中調用flush操作完成的。每一個imm 會被Receiver序列化為一個單行的RowTable,RowTable會得到一個持續遞增的編號標識自己,同時追加寫入到Matrix Container。
當Receiver的中rowtable 的總大小達到了一定的閾值(用戶態可以配置,比如60%),此時Compactor也是空的。當前的Receiver 會停止接受來自客戶端的flush 并 切換成Compactor。同時,一個新的Receiver會被創建出來接受來自DRAM的imm flush。
這個過程并沒有數據遷移,僅僅是兩個組件的狀態切換 并加上 一些打標簽的過程。這個過程也有點像active memtale和 immutable memtable 之間的切換。
RowTable
Receiver接受到的一個imm k-v 序列化為一個RowTable,詳細格式如下
可以看到rowtable中主要分為兩個數據存儲區域:Data, Metadata
- Data : 有序存儲key-value數據,value緊挨著key存儲。(memtable有序,flush的key-value數據默認是有序的)
- Metadata:包含: 索引key、該key在Data區域的偏移地址 offset、該key 在Data區域的頁面Page(邏輯頁,代碼中默認大小是256KB,這個大小也是NVM的基本配置單元)、還有一個pointer,用來加速Matrix Container中的讀,指向前一個RowTable 大于當前key的key index。
通過如上圖也可以看到RowTatable和SST文件的差異,除了Metadata中的block差異,其他方面還是比較相似的。
Compactor
compaction 主要是用來從L0 和 在SSD上的L1中的sst文件選擇能夠進行merge的key-value,并將完成合并的數據形成SST文件寫到L1中。加速 column compaction的過程其實是通過NVM-PMEM的 一個特性:支持按照字節尋址的能力。也就是之前在SSD上以block(4K)為單位讀取數據,而在NVM-PMEM中 最小的讀取單元是(256B),這大大減少了compaction的開銷。
compactor會根據用戶態的配置,將一列keys形成一個column,然后按列進行compaction。當然,compactor本身也會有column compaction的觸發限制,沒有達到限制并不會觸發column compaction。
關于column compaction的細節后續會詳細描述。
Space management
因為Matri Container 是通過pmdk直接操作NVM-PMEM,需要涉及到空間管理。畢竟人家PMEM只是一個存儲設備,如何使用PMEM自己內部是不會感知的。
Matrix KV 維護了一個free list 鏈表 用來管理整個matrix container的空間。當column compactions完成之后會釋放掉一部分空間,如果釋放的空間包含一個page,這個空閑page會被添加到freelist。Receiver 為RowTable分配空間時會從free list取空閑頁。
論文中的數據貌似有問題,8G的contianer 中一個page配置的是4Kb,竟然只用了2^11 個節點就能表示,這里我理解應該是2^20個,或者作者配置的是4M一個page。
比如 代碼中默認一個page大小是256KB,則一個8G的container包含 2^15個節點,每一個鏈表節點區數據使用unsignint 4byte表示,再加上一個8bytes的指針,鏈表節點的元數據總共只需要12bytes存儲。也就是2^14 個節點只需要384KB的存儲空間。
Column Compaction介紹
主體過程就是拿著L1 sst文件的key range 和 matrix container中的compactor形成的column 按行匹配,滿足匹配規則的就將維護的column 和 L1的sst文件進行匹配,最終形成新的sst文件。
觸發column Compaction的要求是Receiver中擁有的RowTable的大小達到了一定量,比如60%,則切換為Compactor之后,comapctor直接就開始調度column compaction相關的邏輯了。實際的代碼中維護了三個水位,類似與之前L0 compaction trigger,slow, stop三個大小。
uint64_t Level0_column_compaction_trigger_size = 7ul * 1024 * 1024 * 1024; //7G trigger
uint64_t Level0_column_compaction_slowdown_size = 7ul * 1024 * 1024 * 1024 + 512ul * 1024 * 1024; //7.5G slowdown
uint64_t Level0_column_compaction_stop_size = 8ul * 1024 * 1024 * 1024; //8G stop
大家看代碼的過程中相關的規范/可讀性 其實可以忽略的,畢竟是學術界的論文demo
完整拆分后的形態如下:
整個column compaction的過程可以分為以下七個詳細步驟:
-
MatrixKV 將key range 按照L1 層的SST文件進行切分。
比如上圖,L1上的幾個SST文件,每個SST文件有smallest_key和largest_key。那么最終的劃分的key_range間隔是
[1,3], [3,8],[8,11], [11,12]… [33,36],這一些key_range會被放在一個vector中{1,3,8,11,12,15,…36} -
Column compaction 會選擇第一個key_range 作為開始,如上圖中的[1,3]
-
接下來進入到了Matrix Container中的compactor,這里此時有多個RowTable,啟用多線程并發讀Rowtable,按列逐個匹配RowTable中的key,看該key是否在挑選的L1的第一個key_range [1,3]。這里啟用的多線程數目經過測試,維持在8個,如果此時 compactor 中有16個RowTable,則每個線程負責讀2個RowTable即可。
-
匹配的過程中除了要求 RowTable的列上滿足在L1挑選的key_range內,如L1的第一個range [1,3]比column中的第一列的某個key 4小,那么將維護的key_range的vector中的下一個range 添加進來,即[3,8],并和[1,3]合并為[1,8]繼續進行匹配。 當一個Column中的key的數量/滿足RowTable的文件大小超過了閾值(默認2個),則形成一個Column boundary。
這個boundary 按照列分割了整個compactor中的RowTable。
-
基于RowTable中的key 邊界構建一個邏輯層的Column.
-
一個Column data 會拿著已經構建好的 key的boundary,也就是key_range和L1中有重疊key的SST文件進行Merge。
Merge的過程也是將MatrixContainer以及L1中目標數據讀取到內存中,進行歸并排序 -
最終的結果會按照SST文件的targe大小形成一個或者多個SST文件,后續的compaction會在SSD上啟用原本rocksdb的邏輯進行。
需要注意的是第一個column compaction L1并沒有sst文件可供劃分key_range,這里是通過判斷 已經選擇的Column 大小是否超過了4個,超過了直接進行column compaction。直接從3步開始,并沒有匹配L1 key_range的邏輯了。
對于Column Compaction的總結
- column compaction的性能提升(加速L0->L1 compaction)的根源還是 利用了NVM-PMEM的按照字節尋址(256B)的能力,Matrix Container中的讀寫都是通過pmdk來直接操作PMEM的。而這樣形態的compaction也是為了適配PMEM 本身的形態。
- 另一個減少WriteStall 的方式則是通過對分離NVM和SSD,兩種compaction的IO帶寬互不影響。
- 再補充一個論文中降低寫放大的優化是通過降低LSM的層數,加大每一層的容量來做的。即WAM= n*AF,保持AF不變的情況下(本身并沒有優化L1以上的compaction邏輯),所以降低層數能夠直接降低寫放大。
論文中沒有直接對比Column Comapction和 rocksdb原生的subcompaction的邏輯,這是一個奇怪的地方。
讀加速 Cross-row Hint Search
因為L0的存儲結構相比于SST文件已經發生了變化,所以需要保證L0的讀能力(RowTable中并沒有像SST文件那樣的filter block和index block)如果不做任何優化,那就相當于在每個 rowtable 幾十萬條key中做二分查找,每一層都得做,這代價顯而易見無法接受。
不選擇bloom filter的原因如下:
- 會帶來額外的開銷在構建filter 上,這個構建過程需要每一個key參與
- bloom filter對點查友好,但對range scan并不友好
只需要在每個RowTable中引入和key數量一樣的一個指針(8bytes),再構建RowTable的過程即可完成指針的指向。
類似如下圖:
-
每個RowTable內是有序的,這個指針只需要指向前一個RowTable第一個不小于自己key的節點即可。比如,RowTable3中的7 指向RowTable2中從左向右的第一個不小于自己key的節點8,依次每個rowtable的節點都指向前一個不小于自己的節點。
-
查找的過程就類似于跳表。每一層rowtable通過 pointer可以定位一個目標key的左右邊界,不用對一整層rowtable的key進行查找。
比如查找12
a. 二分查找定位到12 在10和13之間,10和13各自的pointer向下查找
b. 到了第RowTable2,可以確定的12左右邊界是在8,13之間
c. 依此繼續向前,直到rowTable0
首先查找的肯定是最新的rowTable3, 因為數據是最新的。
MatrixKv 寫入完整流程
如下圖:
- 寫入邏輯先寫 WAL , 設備掉電,能夠保存Mem/imm中的數據,繼續flush rowtable
- 更新DRAM 中的mem,mem滿了switch 為只讀的imm,并創建一個新的mem
- imm flush 到PMEM中,被Receiver序列化形成Matrix Container中的一個RowTable
- Receiver中的RowTable總容量達到了閾值,切換為Compactor
- Compactor 調度Rowtable和L1的 SST文件進行 column compaction
- column compaction的合并過程還是在DRAM中進行,column compaction完成,更新column 的元信息(smallest_key, largest_key, keys_num)到MANIFEST中,方便掉電之后重放 Matrix Container中的 column compaction
- column compaction的結果 會形成SST文件寫入到SSD層的L1中,后續在SSD上按照原生rocksdb的邏輯調度更高層的compaction。
相比于原生rocksdb的邏輯,主要不同的是3,4,5步。
MatrixKV 讀取完整流程
讀取的過程就是逐層讀取了,如下圖。
MatrixKV 性能
這個性能應該是db_bench的測試數據,總體體現的優勢是在隨機寫場景大value下有較為明顯的收益。
而且讀性能并不弱于參與對比的其他rocksdb模型,當然這個隨機寫吞吐提升的數據還需要測試。
在長尾收益中,因為有效提升了L0->L1 compaction的效率,所以長尾收益看起來還是很明顯的。相比于其他的LSM 優化:
- SILK 是本身做了Flush、L0->L1 compaction 以及 Higher Level compactions的優先級調度,保證Flush以及L0->L1的compaction被優先調度,它在長尾的優化效果中還是很明顯的。而Matrix KV比它還好,這個數據需要測試。
- Pebblesdb 為了減少寫放大,引入了類似于跳表節點的gurad,且允許 guard 內部的sst文件有重疊。間接增加了讀放大,所以pebblesdb的長尾數據是合理。
看到長尾優化這里有這么明顯,還是比較吃驚的,數據待測試中。
還有一組寫放大的優化效果,這個數據是通過降低了LSM的層數,增大單層內的LSM空間(論文中給的是扁平化LSM tree的說法),優化效果看起來也比較直觀。
總結
Matrixkv 通過與NVM-PMEM的結合, 在write stall 和 寫放大上有一定的優化。
write stall 優化有兩方面:
- 主要是通過NVM-PMEM的字節尋址能力,加速了Matirx Container上 的column compaction 來降低write stall。
- 引入DRAM --> NVM-PMEM --> SSD 的存儲形態,將compaction的IO搶占分開,互不影響
讀上的優化:
在L0 的Matrix Container中 引入了Cross-row hint search,比較有借鑒意義。通過forward pointer加速讀性能(縮減每次Get的key 范圍)。
寫放大的優化:
扁平化LSM,降低層數,增加單層容量。
存在的問題是,Column compaction并沒有直接和rocksdb原生的sub_compaction 性能進行對比,相比于原生rocksdb性能是否能在L0->L1 compaction的速度進一步提升有待測試。
總結
以上是生活随笔為你收集整理的LSM 优化系列(六)-- 【ATC‘20】MatrixKV : NVM 的PMEM 在 LSM-tree的write stall和写放大上的优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 巅峰塔49层怎么打
- 下一篇: Rocksdb 的优秀代码(二)-- 工