LSM 樹詳解


LSM樹(Log Structured Merged Tree)的名字往往給人一個錯誤的印象, 實際上LSM樹並沒有嚴格的樹狀結構。

LSM 樹的思想是使用順序寫代替隨機寫來提高寫性能,與此同時會略微降低讀性能。

LSM 的高速寫入能力與讀緩存技術帶來的高速讀能力結合受到了需要處理大規模數據的開發者的青睞,成為了非常流行的存儲結構。

HBase、 Cassandra、 LevelDB、 RocksDB 以及 ClickHouse MergeTree 等流行的 NoSQL 數據庫均采用 LSM 存儲結構。

讀寫流程

具體來說 LSM 的數據更新是日志式的,修改數據時直接追加一條新記錄(為被修改數據創建一個新版本),而使用 B/B+ 樹的數據庫則需要找到數據在磁盤上的位置並在原地進行修改。

這張經典圖片來自 Flink PMC 的 Stefan Richter 在Flink Forward 2018演講的PPT

寫入

在執行寫操作時,首先寫入 active memtable 和預寫日志(Write Ahead Logging, WAL)。因為內存中 memtable 會斷電丟失數據,因此需要將記錄寫入磁盤中的 WAL 保證數據不會丟失。

顧名思義 MemTable是一個內存中的數據結構,它保存了落盤之前的數據。SkipList 是最流行的 Memtable 實現方式,Hbase 和 RocksDB 均默認使用 SkipList 作為 MemTable。

當 Active MemTable 寫滿后會被轉換為不可修改的 Immutable MemTable,並創建一個新的空 Active MemTable。后台線程會將 Immutable MemTable 寫入到磁盤中形成一個新的 SSTable 文件,並隨后銷毀 Immutable MemTable。

SSTable (Sorted String Table) 是 LSM 樹在磁盤中持久化存儲的數據結構,它是一個有序的鍵值對文件。

LSM 不會修改已存在的 SSTable, LSM 在修改數據時會直接在 MemTable 中寫入新版本的數據,並等待 MemTable 落盤形成新的 SSTable。因此,雖然在同一個 SSTable 中 key 不會重復,但是不同的 SSTable 中仍會存在相同的 Key。

讀取

因為最新的數據總是先寫入 MemTable,所以在讀取數據時首先要讀取 MemTable 然后從新到舊搜索 SSTable,找到的第一個版本就是該 Key 的最新版本。

根據局部性原理,剛寫入的數據很有可能被馬上讀取。因此, MemTable 在充當寫緩存之外也是一個有效的讀緩存。

為了提高讀取效率 SSTable 通常配有 BloomFilter 和索引來快速判斷其中是否包含某個 Key 以及快速定位它的位置。

因為讀取過程中需要查詢多個 SSTable 文件,因此理論上 LSM 樹的讀取效率低於使用 B 樹的數據庫。為了提高讀取效率,RocksDB 中內置了塊緩存(Block Cache)將頻繁訪問磁盤塊緩存在內存中。而 LevelDB 內置了 Block Cache 和 Table Cache 來緩存熱點 Block 和 SSTable。

Compact

隨着不斷的寫入 SSTable 數量會越來越多,數據庫持有的文件句柄(FD)會越來越多,讀取數據時需要搜索的 SSTable 也會越來越多。另一方面對於某個 Key 而言只有最新版本的數據是有效的,其它記錄都是在白白浪費磁盤空間。因此對 SSTable 進行合並和壓縮(Compact)就十分重要。

在介紹 Compact 之前, 我們先來了解 3 個重要的概念:

  • 讀放大:讀取數據時實際讀取的數據量大於真正的數據量。例如 LSM 讀取數據時需要掃描多個 SSTable.
  • 寫放大:寫入數據時實際寫入的數據量大於真正的數據量。例如在 LSM 樹中寫入時可能觸發Compact操作,導致實際寫入的數據量遠大於該key的數據量。
  • 空間放大:數據實際占用的磁盤空間比數據的真正大小更多。例如上文提到的 SSTable 中存儲的舊版數據都是無效的。

Compact 策略需要在三種負面效應之間進行權衡以適應使用場景。

Size Tiered Compaction Strategy

Size Tiered Compaction Strategy (STCS) 策略保證每層 SSTable 的大小相近,同時限制每一層 SSTable 的數量。當某一層 SSTable 數量達到閾值后則將它們合並為一個大的 SSTable 放入下一層。

STCS 實現簡單且 SSTable 數量較少,缺點是當層數較深時容易出現巨大的 SSTable。此外,即使在壓縮后同一層的 SSTable 中仍然可能存在重復的 key,一方面存在較多無效數據即空間放大較嚴重,另一方面讀取時需要從舊到新掃描每一個 SSTable 讀放大嚴重。通常認為與下文介紹的 Leveled Compaction Strategy 相比, STCS 的寫放大較輕一些[1][2]

STCS 是 Cassandra 的默認壓縮策略[3]。Cassandra 認為在插入較多的情況下 STCS 有更好的表現。

Tiered壓縮算法在RocksDB的代碼里被命名為 Universal Compaction。

Leveled Compaction Strategy

Leveled Compaction Strategy (LCS)策略也是采用分層的思想,每一層限制總文件的大小。

LCS 會將每一層切分成多個大小相近的SSTable, 且 SSTable 是在層內是有序的,一個key在每一層至多只有1條記錄,不存在冗余記錄。

LCS 層內不存在冗余所以空間放大比較小。因為層內有序, 所以在讀取時每一層最多讀取一個 SSTable 所以讀放大較小。在讀取和更改較多的場景下 LCS 壓縮策略有着顯著優勢。

當某一層的總大小超過閾值之后,LCS 會從中選擇一個 SSTable 與下一層中所有和它有交集的 SSTable合並,並將合並后的 SSTable 放在下一層。請注意與所有有交集的 SSTable 合並保證了 compact 之后層內仍然是有序且無冗余的。

LCS 下多個不相關的合並操作是可以並發執行的。

LCS 有一個變體稱為 Leveled-N 策略,它將每一層分為 N 個區塊,層內不再全局有序只在區塊內保證有序。它是 LCS 與 STCS 的中間狀態,與 LCS 相比擁有更小的寫放大,與 STCS 相比擁有更小的讀放大與空間放大。

RocksDB 的壓縮策略

RocksDB 默認采用的是 Size Tiered 與 Leveled 混合的壓縮策略。在 RocksDB 中存在一個 Active MemTable 和多個 Immutable MemTable。

MemTable 被寫入磁盤后被稱為 Level-0 (L0)。L0 是無序的,也就是說 L0 的 SSTable 允許存在重疊。除 L0 外的層都是全局有序的,且采用 Leveled 策略進行壓縮。

當 L0 中文件個數超過閾值(level0_file_num_compaction_trigger)后會觸發壓縮操作,所有的 L0 文件都將被合並進 L1。

因為 L0 是 MemTable 直接落盤后的結果,而熱點 key 的更新可能存在於多個 MemTable 中,所以 L0 的 SSTable 中極易因為熱點 key 而出現交集。

關於 RocksDB 壓縮的更多細節我們可以閱讀官方文檔中的CompactionLeveled Compacton 兩篇文章。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM