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 兩篇文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章