LSM-Tree
1. 什麼是LSM-Tree
LSM-Tree 即 Log Structrued Merge Tree,這是一種分層有序,硬碟友好的資料結構。核心思想是利用磁碟順序寫效能遠高於隨機寫。
LSM-Tree 並不是一種嚴格的樹結構,而是一種記憶體+磁碟的多層儲存結構。HBase、LevelDB、RocksDB這些 NoSQL 儲存都使用了 LSM-Tree。
2. LSM的組成部分
2.1 MemTable
MemTable 是 LSM-Tree 在記憶體中的資料結構,只用於儲存最新的資料,按照 Key 有序地組織這些資料。
LSM-Tree 沒有規定用怎樣的資料結構實現 MemTable,例如 HBase 使用跳錶來保證記憶體中 Key 的有序性。
存在記憶體中的資料會因為斷電丟失,所以我們通常使用 WAL,即預寫日誌的方式來保證資料的可靠。
WAL:預寫日誌,即事務的所有修改在提交之前要先寫入 log 檔案中
2.2 Immutable MemTable
MemTable 達到一定大小後,會轉化為 Immutable MemTable。Immutable MemTable 是將 MemTable 轉為磁碟上的 SSTable 的一種中間狀態。
轉化過程中寫操作由新的 MemTable 處理,過程中不阻塞資料更新操作。
2.3 SSTable
SSTable 是有序鍵值對集合,是 LSM 樹在磁碟中的資料結構。它是一種持久化、有序且不可變的鍵值村儲存結構
SSTable 內部包含一系列可配置大小的 Block 塊。這些 Block 的 index 會被儲存在 SSTable 的尾部,用於幫助快速查詢特定的 Block。當一個 SSTable 被開啟時,index 表會被載入到記憶體,然後根據 key 在記憶體 index 中進行一個二分查詢,查到該 key 對應的磁碟的 offset 後,去磁碟把響應的塊資料讀取出來。
當然,如果記憶體足夠大,可以直接利用 MMAP 的技術把 SSTable 對映到記憶體中,提供更快的查詢。
MemTable 達到一定大小會被 flush 到硬碟中變成 SSTable。在不同的 SSTable 中可能存在相同的 Key 記錄。但這樣會帶來一些問題:
- 冗餘儲存。對於某個 Key,除了最新的記錄,其他記錄都是冗餘無用的。所以我們需要進行 Compact 操作(合併多個 SSTable),來清除冗餘的記錄。
- 讀取時需要從最新的 SSTable 出發進行查詢,最壞情況下藥查詢完所有的 SSTable。可以通過索引或布隆過濾器來優化查詢速度。
3. LSM-Tree讀寫資料
3.1 LSM-Tree寫資料流程
LSM 樹中,我們按照下面的步驟處理寫資料請求。
- 當收到寫請求,先將資料記錄在 WAL Log 中,用作故障恢復。
- 將資料寫入記憶體的 MemTable 中。為了有序,我們往往用跳錶或紅黑樹實現。
- 如果是刪除,則做墓碑標記
- 如果是更新,則新記錄一條資料。
- MemTable 達到一定大小後,在記憶體中凍結,成為不可變的 ImmuTable MemTable。同時也要生成新的 MemTable 來提供服務。
- 記憶體中不可變的 MemTable 被 dump 到硬碟上的 SSTable 中,這也稱為 Minor Compaction。注意 L0 層的 SSTable 是沒有合併的,所以 key 在多個 SSTable 中往往會重疊、冗餘。
- 當每層 SSTable 超過一定大小,就會週期性的進行合併,這也稱為 Major Compaction。這個階段回清除掉榮譽的資料,防止浪費空間。由於 SSTable 都是有序的,可以使用歸併排序進行高效合併。
3.2 LSM-Tree讀資料流程
- 當收到讀請求,現在記憶體中查詢,查詢到就返回。
- 如果沒有查詢到,由記憶體到磁碟,在各級 SSTable 中依次下沉,直到得到結果。
4. LSM-Tree的Compact策略
Compact 是 LSM 樹中的關鍵操作,只有 Compact 的策略合理,才能及時有效地清除冗餘的資料。
先介紹以下幾個概念:
- 讀放大。讀取資料時實際讀取的資料量大於真正的資料量。例如在不同層次的 Table 中查詢。
- 寫放大。寫入資料時實際寫入的資料量大於真正的資料量。例如寫入時觸發 Compact,導致寫入大量資料。
- 空間放大。資料佔用的磁碟空間比真正的資料大小要大很多。即冗餘儲存。
4.1 size-tiered策略
size-tiered 策略保證每層中每個 SSTable 的大小相近,同時限制每一層 SSTable 的數量。
如上圖,每層限制有 N 個 SSTable,每層數量達到 N 後,觸發 Compact 操作來合併這些 SSTable,放入下一層成為更大的 SSTable
當層數越來越大,單個 SSTable 的大小也會越來越大。該策略會導致空間放大比較嚴重。對每一層的 SSTable 來說,每個 key 的記錄也可能存在多份。只有該層執行 Compact 操作才會消除這些冗餘記錄。
4.2 leveled策略
leveled 策略限制每一層總檔案的大小。
leveld 同樣將每一層劃分為大小相近的 SSTable。並保證在一層內全域性有序。這意味著與一個 Key 在每一層至多隻有一條記錄,不存在冗餘記錄。
下面展示 leveled 的 Compact 策略
Ⅰ. L1 總大小超過 L1 本身大小限制。
Ⅱ. LSM 樹從 L1 中選擇至少一個檔案,然後把他和 L2 有交集的部分進行合併。生成的檔案放在 L2。
如下圖,L1 第二個 SSTable 的 Key 的範圍覆蓋了 L2 中前三個 SSTable,那麼就需要將 L1 中第二個 SSTable 與 L2 中前三個 SSTable 執行 Compact。
Ⅲ. 如果 L2 合併後的大小超過 L2 的限制大小。則重複之前的操作,選至少一個檔案然後合併到下一層。
多個不相干的合併可以併發進行
leveled 策略相比 size-tiered 策略來說,每層內的 Key 是有序、不重複的。這樣就很好地控制了冗餘 Key 的量。
5. 查詢優化
查詢過程中我們發現,在原始情況下,我們需要遍歷所有的 SSTable。我們考慮以下方式,嘗試優化查詢的效率。
- 壓縮。SSTable可以進行壓縮,而且不是壓縮整個 SSTable。而是根據區域性性原理將資料分組。每個分組分別壓縮。這樣讀取資料的時候我們就不需要解壓縮整個檔案,而是解壓縮部分 Group 即可讀取。
- 快取。SSTable 除了進行 Compaction,其他情況下是不可變的。所以我們可以將一次掃描到的 Block 進行快取,提高下一次檢索的效率。
- 索引/布隆過濾器。正常情況下,一次讀操作需要讀取所有 SSTable,再將結果合併後返回。但是對某些 Key 而言,有些 SSTable 根本不包含對應資料。所以我們可以為每個 SSTable 新增布隆過濾器。來判斷當前 SSTable 有沒有我們需要的 Key。
- 合併。合併本身肯定可以優化資料的組織情況,提高查詢效率。但是也要注意查詢是非常消耗 CPU 和磁碟 IO 的操作。一般我們選在業務量不大的凌晨等情況進行合併。
6. LSM-Tree和B-Tree的比較
- LSM-Tree 的寫放大問題比 B-Tree 要好一些。因為 B 樹寫入的頁分裂操作實在太消耗磁碟 IO。
- LSM-Tree 可以支援更好的壓縮。由於碎片,B-Tree 無法使用某些磁碟空間,而 LSM-Tree 會定期重寫來消除碎片。
- LSM_Tree 在執行壓縮操作時,很容易發生讀寫請求等待的問題。而 B-Tree 的響應延遲則更具確定性。
- B-Tree 中的每個鍵都位於索引中的每個位置,而日誌結構的儲存引擎可能在不同的段中有相同鍵的多個副本。如果資料庫希望提供嚴格的事務語義,B-Tree 要更容易實現一些,因為鎖可以定義到樹中。