最近練手的項目里用到了LevelDB, 具有很優(yōu)秀的存儲效率,DDIA中有介紹它底層是LSM-tree實現(xiàn)的,今天決定看看LSM-tree,給看到的文章做總結(jié),也給自己想要動手寫了好久的技術博客開個頭吧。
特點
總的來說就是通過將大量的隨機寫轉(zhuǎn)換為順序?qū)懀瑥亩鴺O大地提升了數(shù)據(jù)寫入的性能,雖然與此同時犧牲了部分讀的性能。只適合存儲 key 值有序且寫入大于讀取的數(shù)據(jù),或者讀取操作通常是 key 值連續(xù)的數(shù)據(jù)。這里有一篇比較B樹,B+樹,以及l(fā)sm樹的文章。
存儲模型
- WAL
WAL(write ahead log)也稱預寫log,包括mysql的Binlog等,在設計數(shù)據(jù)庫的時候經(jīng)常被使用,當插入一條數(shù)據(jù)時,數(shù)據(jù)先順序?qū)懭?WAL 文件中,之后插入到內(nèi)存中的 MemTable 中。這樣就保證了數(shù)據(jù)的持久化,不會丟失數(shù)據(jù),并且都是順序?qū)懀俣群芸臁.敵绦驋斓糁貑r,可以從 WAL 文件中重新恢復內(nèi)存中的 MemTable。 - MemTable
MemTable 對應的就是 WAL 文件,是該文件內(nèi)容在內(nèi)存中的存儲結(jié)構(gòu),通常用 SkipList 來實現(xiàn)。MemTable 提供了 k-v 數(shù)據(jù)的寫入、刪除以及讀取的操作接口。其內(nèi)部將 k-v 對按照 key 值有序存儲,這樣方便之后快速序列化到 SSTable 文件中,仍然保持數(shù)據(jù)的有序性。 - Immutable Memtable
顧名思義,Immutable Memtable 就是在內(nèi)存中只讀的 MemTable,由于內(nèi)存是有限的,通常我們會設置一個閥值,當 MemTable 占用的內(nèi)存達到閥值后就自動轉(zhuǎn)換為 Immutable Memtable,Immutable Memtable 和 MemTable 的區(qū)別就是它是只讀的,系統(tǒng)此時會生成新的 MemTable 供寫操作繼續(xù)寫入。之所以要使用 Immutable Memtable,就是為了避免將 MemTable 中的內(nèi)容序列化到磁盤中時會阻塞寫操作。 - SSTable
SSTable 就是 MemTable 中的數(shù)據(jù)在磁盤上的有序存儲,其內(nèi)部數(shù)據(jù)是根據(jù) key 從小到大排列的。通常為了加快查找的速度,需要在 SSTable 中加入數(shù)據(jù)索引,可以快讀定位到指定的 k-v 數(shù)據(jù)。
SSTable 通常采用的分級的結(jié)構(gòu),例如 LevelDB 中就是如此。MemTable 中的數(shù)據(jù)達到指定閥值后會在 Level 0 層創(chuàng)建一個新的 SSTable。當某個 Level 下的文件數(shù)超過一定值后,就會將這個 Level 下的一個 SSTable 文件和更高一級的 SSTable 文件合并,由于 SSTable 中的 k-v 數(shù)據(jù)都是有序的,相當于是一個多路歸并排序,所以合并操作相當快速,最終生成一個新的 SSTable 文件,將舊的文件刪除,這樣就完成了一次合并過程。
LSM tree的寫入
1,當收到一個寫請求時,會先把該條數(shù)據(jù)記錄在WAL Log里面,用作故障恢復。
2,當寫完WAL Log后,會把該條數(shù)據(jù)寫入內(nèi)存的SSTable里面(刪除是墓碑標記,更新是新記錄一條的數(shù)據(jù)),也稱Memtable。注意為了維持有序性在內(nèi)存里面可以采用紅黑樹或者跳躍表相關的數(shù)據(jù)結(jié)構(gòu)。
3,當Memtable超過一定的大小后,會在內(nèi)存里面凍結(jié),變成不可變的Memtable,同時為了不阻塞寫操作需要新生成一個Memtable繼續(xù)提供服務。
4,把內(nèi)存里面不可變的Memtable給dump到到硬盤上的SSTable層中,此步驟也稱為Minor Compaction,這里需要注意在L0層的SSTable是沒有進行合并的,所以這里的key range在多個SSTable中可能會出現(xiàn)重疊,在層數(shù)大于0層之后的SSTable,不存在重疊key。
5,當每層的磁盤上的SSTable的體積超過一定的大小或者個數(shù),也會周期的進行合并。此步驟也稱為Major Compaction,這個階段會真正 的清除掉被標記刪除掉的數(shù)據(jù)以及多版本數(shù)據(jù)的合并,避免浪費空間,注意由于SSTable都是有序的,我們可以直接采用merge sort進行高效合并。
LSM tree 的讀取
LSM Tree 的讀取效率并不高,當需要讀取指定 key 的數(shù)據(jù)時,先在內(nèi)存中的 MemTable 和 Immutable MemTable 中查找,如果沒有找到,則繼續(xù)從 Level 0 層開始,找不到就從更高層的 SSTable 文件中查找,如果查找失敗,說明整個 LSM Tree 中都不存在這個 key 的數(shù)據(jù)。如果中間在任何一個地方找到這個 key 的數(shù)據(jù),那么按照這個路徑找到的數(shù)據(jù)都是最新的。
在每一層的 SSTable 文件的 key 值范圍是不重復的,所以只需要查找其中一個 SSTable 文件即可確定指定 key 的數(shù)據(jù)是否存在于這一層中。Level 0 層比較特殊,因為數(shù)據(jù)是 Immutable MemTable 直接寫入此層的,所以 Level 0 層的 SSTable 文件的 key 值范圍可能存在重復,查找數(shù)據(jù)時有可能需要查找多個文件。
讀取的優(yōu)化
因為這樣的讀取效率非常差,通常會進行一些優(yōu)化,例如 LevelDB 中的 Mainfest 文件,這個文件記錄了 SSTable 文件的一些關鍵信息,例如 Level 層數(shù),文件名,最小 key 值,最大 key 值等,這個文件通常不會太大,可以放入內(nèi)存中,可以幫助快速定位到要查詢的 SSTable 文件,避免頻繁讀取。
1,壓縮
SSTable 是可以啟用壓縮功能的,并且這種壓縮不是將整個 SSTable 一起壓縮,而是根據(jù) locality 將數(shù)據(jù)分組,每個組分別壓縮,這樣的好處當讀取數(shù)據(jù)的時候,我們不需要解壓縮整個文件而是解壓縮部分 Group 就可以讀取。
2,緩存
因為SSTable在寫入磁盤后,除了Compaction之外,是不會變化的,所以我可以將Scan的Block進行緩存,從而提高檢索的效率
3,索引,Bloom filters
正常情況下,一個讀操作是需要讀取所有的 SSTable 將結(jié)果合并后返回的,但是對于某些 key 而言,有些 SSTable 是根本不包含對應數(shù)據(jù)的,因此,我們可以對每一個 SSTable 添加 Bloom Filter,因為布隆過濾器在判斷一個SSTable不存在某個key的時候,那么就一定不會存在,利用這個特性可以減少不必要的磁盤掃描。
4,合并
這個在前面的寫入流程中已經(jīng)介紹過,通過定期合并瘦身, 可以有效的清除無效數(shù)據(jù),縮短讀取路徑,提高磁盤利用空間。但Compaction操作是非常消耗CPU和磁盤IO的,尤其是在業(yè)務高峰期,如果發(fā)生了Major Compaction,則會降低整個系統(tǒng)的吞吐量,這也是一些NoSQL數(shù)據(jù)庫,比如Hbase里面常常會禁用Major Compaction,并在凌晨業(yè)務低峰期進行合并的原因。
列下以后可以看的參考資料:
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf
https://cloud.tencent.com/developer/article/1441835
http://www.lxweimin.com/p/1b438f850844