基於LSM樹的儲存機制簡述

寒光瀲灩晴方好 發表於 2022-11-25

下午聽了關於MyRocks-PASV的研究講座,很有意思所以學習了一下LSM樹的一些簡單的底層原理。現在整理一下


我們都知道目前Key:Value型的資料庫普遍較之關係型資料庫有著更好的表現,為什麼會有這樣的一個差異呢?關鍵就在於儲存形式和讀寫機制的不同。Key:Value型資料庫可以透過LSM Tree(Log-Structured-Merge-Tree)來進行儲存,而以MySQL為代表的關係型資料庫則以B+樹的形式來組織聚簇索引檔案和二級索引檔案來進行儲存。二者的實現機制由於是否順序寫產生了很大的差別。

LSM樹的具體機制

LSM樹其實並不是一個真正意義上的樹形結構,其核心特點是利用順序寫來提高寫效能,順序寫很好理解。舉個很多人都背過的例子就是MySQL的InnoDB引擎在寫入內容時會首先透過WAL機制,先將改動寫進磁碟中的redo log這個物理日誌中,物理日誌記錄的是對某表的某資料頁某偏移量處做了某更新,然後再找機會把redo log中的內容刷進磁碟中真正儲存資料的地方。這麼做的好處就是因為寫入物理日誌只需要接著之前寫到的位置往下寫就行,不需要隨機存取磁碟內容,可以進行的相對快速。

但問題就是,MySQL終究還是要把redo log中的操作寫入磁碟中真正儲存資料的地方,而這個動作就限制了關係型資料庫的效能發揮。而LSM樹就是直接將WAL機制作為儲存資料的主要機制來實現資料庫的存取。

LSM樹示意圖

如圖所示,LSM樹有以下三個重要組成部分

  • MemTable
  • Immutable MemTable
  • SS Table

MemTable

MemTable是在記憶體中的資料結構,用於儲存最近更新的資料操作,這裡會按照Key有序地組織這些資料。至於具體如何組織這些資料以使其按照Key有序,LSM樹並沒有做出相應的規定,可以自行實現。

要注意的是因為資料暫時儲存在記憶體中,那麼如果斷電了就會存在丟失資料的風險,因此這裡還是會跟InnoDB一樣透過WAL機制來寫入磁碟,以保證資料不會因為崩潰或斷電而丟失。

Immutable MemTable

當MemTable儲存的資料達到閾值後,就會將該Memtable就地轉化成Immutable MemTable。Immutable MemTable是將轉MemTable變為SSTable的一種中間狀態。你可以理解其為一個暫存檔案,該檔案不再繼續寫入,而是等待刷入磁碟的SSTable中。之後的寫入工作則由一個新建立的MemTable處理。

SSTable(Sorted String Table)

其本質是有序鍵值對的集合檔案,是LSM樹在磁碟中的資料結構,我們可以建立key的索引來加快查詢。

重點來了,LSM樹會將所有的資料插入、修改、刪除等操作記錄(注意是操作記錄)儲存在記憶體之中,當此類操作達到一定的資料量後,再批次地順序寫入到磁碟當中。注意,這與B+樹不同,B+樹資料的更新會直接在原資料所在處修改對應的值,但是LSM數的資料更新是日誌式的,就跟上面說的WAL機制的redo log一樣,一條資料更新是直接將更新記錄寫進log的最後來完成的。而這樣設計的目的就是為了順序寫,我們只需要不斷地將Immutable MemTable刷入磁碟進行持久化儲存即可,不用去修改之前的SSTable中的key,也就保證了順序寫,提升了效率。

不過不斷地向後寫也就意味著,在不同的SSTable中,可能存在相同Key的記錄,顯然最新的那條記錄才是真實的。那麼這樣設計的雖然大大提高了寫效能,但同時也會帶來一些問題:

  • 冗餘儲存,對於某個key,除了最新的那條記錄外,其他的記錄都是冗餘無用的,但是仍然佔用了儲存空間。這裡LSM樹引入了Compact操作(合併多個SSTable)來清除冗餘的記錄。
  • 讀取時需要從最新的記錄反向進行查詢,直到找到某個key的記錄。最壞情況需要查詢完所有的SSTable,這裡可以透過索引/布隆過濾器來最佳化查詢速度。

Compact策略

講到SSTable,我們可以看到這裡從MemTable到SSTable一路下來全是順序寫的,問題是磁碟容量並不是無限的,何況SSTable中實際上的真實資料只有最新的那一條,所以如何對SSTable進行Compact來釋放空間是很重要的。

Compact釋放空間的策略講到底就是圍繞著三大效能的權衡策略。即在以下三個問題中,我們必須至少接受其中之一:

  • 讀放大:讀取資料時實際讀取的資料量大於真正的資料量。
    • 例如在LSM樹中需要先在MemTable檢視當前key是否存在,不存在繼續從SSTable中尋找。
  • 寫放大:寫入資料時實際寫入的資料量大於真正的資料量。
    • 例如在LSM樹中寫入時可能觸發Compact操作,導致實際寫入的資料量遠大於該key的資料量。
  • 空間放大:資料實際佔用的磁碟空間比資料的真正大小更多。
    • 上面提到的冗餘儲存,對於一個key來說,只有最新的那條記錄是有效的,而之前的記錄都是可以被清理回收的。

size-tiered 策略

簡單來說就是把SSTable分層,限制各層的SSTable數量,並且要求每一層的SSTable的大小都要相近,注意不是相同。當上層SSTable過多時,就將多出來的SSTable合併,作為一個更大的SSTable存入下一層。

可以看出,當層數達到一定數量時,最底層的單個SSTable的大小會變得非常大。而且這種策略會導致空間放大嚴重。因為即使對於同一層的SSTable,每個key的記錄是可能存在多份的,只有當該層的SSTable執行compact操作才會消除這些key的冗餘記錄,那麼積累到整個結構中,冗餘的記錄佔比可想而知。

leveled 策略

leveled策略也是採用分層的思想,與size-tiered策略不同的地方在於,leveled策略會將每一層切分成多個大小相近的SSTable,並且保證這些SSTable是這一層是全域性有序的,而這就意味著一個key在每一層至多隻有1條記錄,不存在冗餘記錄。

Leveled策略是透過合併策略來保證每一層的SSTable全域性有序的。具體實現方法為當某一層總大小超過限制,那麼選擇該層中與下一層存在交集的SSTable檔案進行合併並放入下一層,重複這個操作直到該層大小恢復到限制以內為止。並且,多個不相干的合併是可以併發進行的,這大大提高了效率。

這種策略極大緩解了空間放大的問題,但是代價是寫放大問題更加突出。在最壞的情況下,如果該層某個SSTable的key的範圍跨度非常大,覆蓋了下一層所有key的範圍,那麼進行Compact時將涉及下一層的全部資料。

不過這裡只是簡單的聊了一下這些機制,具體使用LSM樹進行儲存的那些應用中,基本都對它們進行了進一步的最佳化。這裡不再細說