作為目前資料庫引擎的兩種主要資料結構,LSM-tree和B+-tree在業界已經有非常廣泛的研究。相比B+-tree,LSM-tree犧牲一定的讀效能以換取更小的寫放大以及更低的儲存成本,但這必須建立在已有的HDD和SSD的基礎上。
探索前沿研究,聚焦技術創新,本期DB·洞見由騰訊雲資料庫高階工程師王巨集博進行分享,主要介紹一篇2022年FAST的論文,主題為“基於硬體透明壓縮的B+樹優化”。本次分享的論文針對可計算儲存SSD(支援硬體透明壓縮)提出了三種有趣的設計方法,從而極大地減少了B+-tree的寫放大(10X)以使其接近甚至超越LSM-tree。以下為分享實錄:
完整視訊:https://v.qq.com/x/page/p3342om19py.html
一、論文簡介
本期分享的是2022年FAST的一篇論文,主題是基於硬體透明壓縮的B+樹優化,該論文在內容上主要分為三個部分:
第一部分是背景介紹,作者分別介紹了現有的B+樹及其軟體壓縮、儲存硬體透明壓縮、B+樹和LSM-tree的簡要對比以及B+樹寫放大的構成分析。
第二部分是演算法部分,作者針對性地提出了三種設計來優化B+樹的寫放大:
確定性的page shadowing來解決頁面原子寫引入的寫放大。
通過頁面本地增量日誌來減少刷髒頁引入的寫放大。
通過稀疏日誌來減少redo日誌引入的寫放大。
第三部分是實驗部分,作者通過前面所述的三個方法實現了一個新的B+樹(我們稱之為B−-tree為以示區別),並詳細比較了多種條件下的測試結果。
二、背景部分
2.1 現有的B+樹及其軟體壓縮
我們熟悉的開源資料庫有很多都使用B+樹作為儲存引擎,比如MySQL、MongoDB、PostgreSQL等,騰訊雲資料庫TDSQL-PG也是基於B+樹來實現的。B+樹的結構如下方的左圖所示,它通過一個個的資料頁面構成一個樹狀結構以提供索引加速資料查詢。常見的資料頁面大小有8k和16k,通過資料頁面壓縮可以減少B+樹的空間佔用以降低成本,此外還可以減少B+樹的寫放大。
在前面所提到的基於B+樹實現的資料庫中,MySQL、MongoDB都實現了B+樹的軟體壓縮特性,TDSQL-PG的商業化版本也有實現。軟體壓縮具有不依賴於特定硬體、靈活性高的優點,但美中不足的是,受限於現有的IO 4K對齊的約束會產生一些額外的空間浪費。比如下方右圖,一個16K的資料庫頁面經過壓縮後產生了5K的資料。受限於IO 4K對齊的要求,當落盤時這5K的資料實際上要佔用8K的空間,產生了額外的3K的空間浪費。
常見的B+樹壓縮演算法有Lz4、zlib、ZSTD,後面所提的LBA則是指logical block adressing,即邏輯上的磁碟塊大小。
2.2 儲存硬體透明壓縮
我們先簡單回顧下快閃記憶體的特性。如下圖所示,快閃記憶體支援三種基本操作:讀、寫、擦除。快閃記憶體的讀、寫都以page為單位,一般是4K,擦除則是以block為單位,一個block一般包含多個page,比如包含64個page。之所以快閃記憶體需要支援擦除,是由於快閃記憶體的寫比較特殊,一個page只能在擦除之後一次寫入,而不能更新。但從使用者的角度來看,SSD需要支援更新,因此我們在SSD裡會有一個FTL層來實現該邏輯,即下圖中間藍色部分所示。
CSD,顧名思義是一種帶有計算能力的儲存。如下圖所示,一個通用的CSD在儲存層可以支援API呼叫來進行計算下推。硬體透明壓縮儲存作為CSD的一種,支援在其內部對資料進行壓縮和解壓,後續我們提到的CSD主要是指帶有硬體透明壓縮的CSD。
對比最下方的兩張圖,左邊是普通的SSD,右邊是CSD。可以看到普通SSD的LBA和快閃記憶體都是以頁面為基礎,且都是4K大小,可以由FTL直接進行對映。CSD和SSD類似,都對外提供了4K的LBA介面,但4K的資料經過壓縮後長度不一定,所以在CSD內部,FTL的快閃記憶體管理需要支援對變長資料的管理,這也使得CSD的FTL變得更加複雜。如此一來CSD能帶來什麼好處呢?
總體來看,CSD的好處體現在它有更優的成本、更好的效能,但在此不做詳細討論,我們主要關注CSD的技術特點。CSD可以實現邏輯地址和實體地址的解耦。如下方左圖所示,對一個實際快閃記憶體大小隻有4T的CSD來說,它可以對外提供遠大於快閃記憶體大小的LBA。另外,如下方右圖所示,對於稀疏資料的寫入可以進行透明壓縮而不產生空間浪費。基於CSD的這兩個特性,我們在上層應用如資料庫中,可以做一些針對性的設計,以進一步利用CSD的特性。
2.3 B+樹和LSM-tree的簡要對比
我們先回顧LSM-tree的寫入流程。如右上圖所示,一條資料記錄的寫入,會先寫log,再進入memtbale,memtable寫滿之後會變成 immutable memtable, 再與L0的SSTable進行合併,上層的SSTbale也會在合適的時機與下層SSTable進行合併。
我們再簡要回顧B+樹的寫入流程。一條記錄的寫入也是先寫入log,再寫入B+樹的資料頁面,當資料頁面寫滿時就需要進行頁面分裂,所以大部分時間B+樹的頁面其實是不滿的。另外,由於B+樹是直接操作目標頁面,當記憶體不夠時就會涉及到頁面的淘汰問題,這時就需要刷髒頁。極端場景是每寫一條記錄就需要刷一次髒頁,如果一條記錄是128位元組,頁面是8k,那麼對應的寫放大就是64倍。
B+樹和LSM-tree在寫方面的主要區別是:LSM-tree是一個緊密排布的結構,可以佔用更小的寫空間以及產生更小的寫放大。B+樹的寫放大與資料集的大小成正相關,與單條記錄的大小負相關。對於資料完全可以放入記憶體的場景,B+樹可以產生比LSM-tree更小的寫放大。因此這篇論文聚焦在大資料集以及小資料記錄的場景,也就是說,B+樹在該場景下寫放大明顯劣於LSM-tree。
作者通過在CSD上執行一個隨機寫入的測試,初步展示了B+樹和LSM-tree的差異。結果如右下圖所示,可以看到LSM-tree佔用的邏輯空間更小,這與LSM-tree的資料緊密排列有關,經過CSD壓縮之後,B+樹的物理空間佔用也大幅度減小。此外,通過對比發現,LSM-tree的寫放大顯著小於B+樹,因此本論文的目標就是需要通過改進B+樹的實現來大幅度減小B+樹的寫放大。
2.4 B+樹寫放大的構成分析
B+樹的寫放大構成主要包含三個部分:
為了保證事務原子性採用redo log引入的寫放大 ;
由於lru buffer淘汰或者checkpoint 刷髒頁而引入的寫放大;
為了保證頁面寫的原子性而引入的寫放大,如MySQL的 double write。
當考慮CSD的壓縮特性時,公式1所列的寫放大會引入表示壓縮比的係數α ,其含義為 壓縮後的資料大小比壓縮前的資料大小,其位於0-1之間,也就是說,CSD會降低寫放大。
三、演算法部分
3.1 Deterministic Page Shadowing
演算法1是通過確定性的page Shadowing來減少page原子寫引入的寫放大,其本質上是copy on write,一次頁面刷盤寫只需要寫一次即可。具體實現如右圖所示,1個資料頁面在磁碟上會有2倍的連續LBA空間與之對應,這個LBA被劃分為兩個slot,兩個slot輪流寫入以實現copy on write,從而保證每次寫入不管是否成功,頁面的前映象總是保持穩定。在頁面寫入成功後,可以通過trim 操作清理前一個slot的物理空間佔用。在頁面的寫失敗時,我們可以通過 checksum進行檢測,並通過頁面前映象+redo恢復出最新的頁面。對於寫成功後crash保留兩個完整頁面的場景,則可以通過比較頁面的LSN找到最新的頁面,並trim掉舊的頁面。
本方法帶來的一個負面影響就是每次讀需要產生額外的資料傳輸,但是作者認為pcie的傳輸能力遠大於flash的速度,因此不成問題。此外,對於支援計算下推的CSD,我們可以將該邏輯下推,也可以避免掉額外的資料傳輸。
3.2 Localized Page Modification Logging
演算法2是通過本地修改日誌來減少刷髒頁引起的寫放大。如右圖所示,一個資料頁面在儲存層對應一個資料頁面+一個4K增量log頁面,兩者的邏輯地址連續。在記憶體中資料頁面被分為k個segment,每個segment大小是Ds,每次頁面修改所涉及到的segment會在bitmap f裡面記錄。當需要刷髒頁時,我們可以通過bitmap f找到所有的修改增量,並把它記為δ,再把δ、bitmap f、補零後的資料一起寫入盤中。以一個16K的資料頁面為例,對應的寫放大會減少75%。經過CSD透明壓縮後,寫放大可進一步減小。
在該方法中,由於額外引入了增量日誌,會造成一定的物理空間的放大,我們可以通過引數T來控制物理空間的放大。當增量日誌大於T時直接刷髒頁並重置增量日誌,T越大寫放大越小,但是對應的物理空間放大就越大。讀取頁面時我們需要讀原始資料頁面+增量日誌部分,再構造最新的資料頁面。由於讀頁面需要額外讀所以會造成一定的讀放大,但由於資料頁面的地址和增量日誌LBA是連續的,因此一次讀請求即可解決。另外,增量日誌相對較小,所以可以認為這部分的讀放大影響較小。
3.3 Sparse Redo Logging
演算法3是稀疏日誌,用於減少Redo log造成的寫放大。目前常見的Redo log的實現如左圖所示,日誌採用連續緊密排列,事務1提交時日誌L1落盤,事務2提交時由於日誌buffer未滿,因此會在L1的buffer中追加L2,L1和L2一起落盤,事務3提交時,L1、 L2 需要連同L3再落盤一次,直到當前日誌buffer頁面被寫滿,才會開啟新的頁面。在該例子中,從flash的角度看,L1被寫了3次,後兩次都是額外的寫放大。當採用右圖所示的稀疏日誌時,每次日誌寫入都是用一個新的頁面,頁面經過透明壓縮後落盤,則完全可以避免寫放大的問題,另外由於透明壓縮的存在也不會產生額外的空間佔用。此外稀疏日誌也是可以用於LSM-tree的。
四、實驗部分
在實驗部分,作者通過前述的三個優化方法實現了一個新的B+樹(命名為B-樹以示區別),同時還實現了一個Baseline的B+樹,並且與Rocksdb、WiredTiger做了比較全面的實驗對比。
4.1 Experiments with Log-Flush-Per-Minute
在實驗1中,Redolog的刷盤頻率被設定為每分鐘1次,以遮蔽掉Redo log寫放大的影響。通過不同的資料集大小、單條記錄的大小以及B+樹頁面大小設定,作者比較全面地評估了B-樹的特性,得出初步的結論:B-樹的寫放大已經比較接近於LSM-tree,即右圖中所有的橙色、綠色和紫色的部分比較接近。而Baseline B+tree和WiredTiger這兩種標準的B+Tree,它們的寫放大都比較大。
通過實驗資料的對比,我們還可以發現:
B+樹的寫放大與Record size負相關,即Record size越小其寫放大越大。以前面提到的極端例子為例,每條Record都會引起一次刷髒頁,這時的寫放大是Page size比Record size。如果Page size固定,當Record size越小,其寫放大會越大。
B+樹的寫放大與併發數負相關,即併發數越大其寫放大越小。我們可以理解為,當併發數比較高時,一個頁面裡可能會產生更多的更新,一次刷髒頁就會有更多的Record進來。
LSM-Tree的寫放大與資料集正相關。當資料集比較大時,做不同層之間的merge,它的寫放大會變大。
4.2 Experiments with Log-Flush-Per-Commit
在實驗2中,Redo log的刷盤頻率被設定為每次事務提交,這也是我們在實際生產中為保證不丟資料而常用的方法。作者用一個單獨的圖來展示log部分的寫放大,我們可以看到LSM-tree和B+樹的Redo log的寫放大都跟Record size相關,Record size越小,寫放大越大,同時與併發數負相關,隨著併發數的上升寫放大會減小。這是由於併發數的上升實際上會產生更多日誌的合併,即每次寫盤時會有更多的事務的提交日誌一起寫入,因此寫放大減少了。對B-樹而言,由於採用了稀疏日誌,所以其在不同Record size大小和不同併發數下都保持很小的日誌寫放大。
圖12展示了一個考慮日誌寫放大後的整體寫放大。我們可以看到,B-樹相對LSM-tree而言,它的寫放大進一步降低,除了圖中紅框部分外,其他場景都比LSM-tree更優。
如果把兩個不同的日誌刷盤方式放在一起對比,我們可以發現,Redo log的寫放大對B-樹而言總體保持穩定,因為它使用了稀疏Redo log。當其他B+樹和LSM-tree採用每事務提交時,兩者的寫放大顯著提高,尤其是在併發數比較小的情況下,Redo log日誌所引起的寫放大佔比較高。
4.3 Impact of Threshold T
在實驗3中,作者對演算法2中提到的本地頁面修改增量日誌的演算法做了驗證。引數T表示當增量日誌達到多少量時直接刷資料頁面並重置增量日誌,引數Ds表示頁面分割的segment的大小。如圖14和表格2,我們可以看到當T越大時,B-樹的寫放大會越小,但物理空間佔用則會越大,這與我們之前的理論分析一致,所以這兩個引數在實際中需要trade off。
圖13展示了壓縮對於物理空間佔用的影響。由於B-樹需要額外的4k頁面佔用,所以B-樹的邏輯空間佔用顯著高於其他B+樹,但在經過CSD壓縮後,實際物理空間的佔用放大則縮小了,相比B+樹已經不明顯了。
4.4 Speed Performance Evaluation
在實驗4中,作者對比了三種場景下的B-樹相對於其他幾種樹的效能。
在隨機點查場景中, B- tree 和 LSM-tree 相當,但兩者都小於B+樹。這與B- tree採用本地頁面的增量日誌刷髒頁有關,因為每次讀取需要重新通過增量日誌來構造最新的資料頁面,這會引入額外的開銷 。
對隨機範圍查詢而言,B-樹和B+tree的效能相當,優於LSM-tree。作者認為,由於一次查詢多條記錄,B-樹引入的讀開銷佔比變小了,所以其效能比較高。
在隨機寫場景中,B-樹稍微優於LSM-tree,兩者都顯著優於B+樹,這是由於前兩者都擁有更小的寫放大。但需要強調的是,由於B-樹和B+樹都涉及到buffer 淘汰問題,所以雖然是隻寫負載但實際的IO卻是讀寫混合的,因為B-樹需要讀取額外的4k增量日誌頁面,因此讀IO比B+tree有所放大,其相對B+樹的效能提升也就沒有像寫放大減少得那麼顯著。
五、擴充與總結
最後分享一個CSD和PG結合起來的有趣實驗。PG中有個名為 heap only tuple update的機制,用於優化非索引列更新。如下圖所示,當更新一個元組時,如果當前元組所在的heap表頁面有空閒空間,就可以直接在當前頁面插入一條新的元組,再把老元組的ctid指向新元組即可,這樣可以避免去找新的資料頁面寫入以及在索引頁面中寫入新的記錄。PG中還有一個fillfactor的引數來控制heap表的填充率,我們可以利用較小填充率來換取非索引列更新的效能。當引入CSD之後,還可以進一步避免造成物理空間佔用的放大,比較完美地解決了以往需要進行物理空間佔用和效能trade off的問題。
綜上所述,這篇論文提供了一個不同的視角去比較B+樹和LSM-tree,讓我們更深入地理解B+樹和LSM-tree。作者通過三種實現比較簡單但卻非常巧妙的方法來改進B+樹,在CSD上也取得了很好的效果,為我們帶來了很好的啟發。
參考文獻
[1] Closing the B+-tree vs. LSM-tree Write Amplification Gap on Modern Storage Hardware with Built-in Transparent Compression
[2] The True Value of Storage Drives with Built-in Transparent Compression: Far Beyond Lower Storage Cost
[3] Understanding Flash: Blocks, Pages and Program / Erases
[4] B+Tree index structures in InnoDB
[5] PostgreSQL與透明壓縮
[6] The Internals of PostgreSQL
[7] WiscKey: Separating Keys from Values in SSD-Conscious Storage