一 背景
本來想寫點B+樹的,不過B+樹因為用在Mysql等關系型數據庫中,大家都比較了解了,而LSM樹這種索引設計思路主要用在NoSql中,如果沒有接觸過NoSQL數據庫的朋友可能了解不多,就開一篇介紹下,參考了不少的文章和資料。
LSM樹是Log Structured Merge Trees
的簡稱(這里面的日志,不一定是指我們程序的日志,也是指一類以時間為其中維度的大批量的樹)。在NoSQL數據庫中有廣泛的應用(比如LevelDB和HBASE等)。嚴格來講并不是一種索引結構,是一種索引設計的整體架構性的東西。B+樹作為關系型數據庫常用的索引存儲結構,本身挺優秀的,支持范圍查詢,隨機訪問,增刪改查都支持的不錯,只是如果在修改的時候,B+樹的聚簇索引數據是存在葉子節點的,需要磁盤的隨機讀寫,隨機寫的性能不夠好。磁盤的隨機讀寫的性能不好,順序讀寫的性能可以達到和內存一個數量級別,對此的優化很容易想到采用批量順序寫替代磁盤的隨機寫。
LSM樹適合的場景是寫入的量很大,查詢的量比較小,而且查詢查詢近期的數據,老的數據一般很少查詢的場景,比較適合用LSM樹。
二 LSM樹的關鍵思想
Log Structured
采用日志追加的方式,而不是采用隨機寫,追加采用順序寫,所以性能好。另一個重要的思想是Merge Trees
, 數據寫入的時候,先寫入到內存的樹中,可以是紅黑樹,也可能是B+樹,有的采用跳表的內存數據結構。
當內存中的樹大小達到一定規模后,將內存中的樹和磁盤中的樹進行合并,這就是合并樹的來源。數據開始寫入到內存的時候,如果突然斷電,或者系統異常,會導致數據丟失,為了防止數據丟失,采用WAL(Write Ahead Log 預寫日志技術)將數據第一時間寫入到磁盤文件中,由于是順序寫,所以性能上不用擔心。在發生異常的時候,可以通過WAL文件進行數據恢復。
如上圖所示,LSM樹,整體由內存部分數據結構和磁盤上的兩類文件組成。
內存中的數據結構為memtable和immutable Memtable兩部分組成,前一個可讀可寫,后一個是只讀的,它們兩是一樣的數據結構。immutable Memtable 由于是只讀的,可以在刷入到磁盤的時候不用加鎖,提升性能。那么數據是如何讀寫的那?
寫數據:
- 寫請求來之后,寫入到WAL中和內存的memtable中,memtable可以是紅黑樹或跳表等。
- 如果memtable滿了,就新申請內存構建一個新的memtable,且將原來的memtable轉為只讀的
immutable memtable
,適當時機刷到磁盤中,保存為一個SStable文件。 - 刷新到磁盤之后,就可以對WAL日志進行截斷,這樣防止WAL日志的無限擴大的問題。
-
immutable memtable
保存到磁盤中,如果直接和磁盤上的文件進行合并,因為內存中的數據少,磁盤中的很大,合并的數據很少,卻占用了大量的IO磁盤,這肯定不行。所以像LevelDB
采用延遲合并的方式,每層滿了之后,都會和下一層進行滾動合并。
讀數據:
- 讀數據的時候,先從內存中的memtable中讀取。
- 如果沒有,則到
immutable memtable
中讀取。 - 如果仍然未查詢到數據,則到緩存中讀取,緩存里面包括數據緩存和索引緩存。
- 如果仍然沒有找到,則從
Level 0
層開始查找,由于Level 0
層的SSTable沒有合并,所以數據是有重合的,所以每個SSTable
都需要查找,Level 0
層的只有4MB(level DB)中,所以也比較快。 - 如果Level 0 層,沒有找到數據,則下沉到下一層
Level 1
層繼續查找,一層以及以后層次SSTABLE文件是不重合的,所以可以通過快速定位到SSTable
文件中,進行查找,找到了或所有的層都查找完畢后返回。
三 SSTable
SSTable
即Sorted String Table
,聽著蠻高大上,其實就是排序的有序鍵值對集合,是存儲在文件中的結構。
我們保存哈希表為磁盤文件中,為了提升查詢速度,一般還保存一個索引文件,保存key和offset的位置,通過key快速獲得offset,再打開文件,直接定位到offset位置。
但是哈希表沒有順序的,訪問不同的key時候需要隨機的磁盤訪問,而且哈希表沖突的時候,需要復雜的處理邏輯,還無法支持范圍查詢。
簡單改變下,我們按照key-value對的順序按鍵排序,這種格式稱為排序字符串表即SSTable。
此結構將數據部分和索引部分分離,在查詢的時候,不需要將整個SSTable文件都讀入到內存中。而是先讀入索引部分,可以通過布隆過濾,快速判斷要查的key是否存在,如果不存在,就不用再讀數據部分了。
索引部分的Index Block
記錄采用key:offset:size
形式,key
為每個Data Block
最小的key,offset
記錄Data Block
的起始位置,size
為每個Data Block
的大小。每個Data Block
采用順序存儲的方式,我們可以方便進行二分查找。
四 索引加速
如果每次都從 SSTable
中加載數據都需要從磁盤讀取數據,性能比較差。所以LevelDB
中設計了內存的緩存區。緩存區包括兩個部分,一部分為最近使用的Index Block
,另一個部分為Data Block
,這樣如果我們搜索的索引和數據都在內存中,就不用從磁盤中讀取,提升了查詢性能。
五 Level中SSTable合并
首先為什么需要SSTable
合并那,那是因為,隨著數據的增大,寫的SSTable
文件越來越多,而且隨著對key
的更新和刪除,這種需要刪除的數據越來越多,為了減少文件數量和清理無效數據,就需要進行compact
,即將多個SSTable
文件合并成一個SSTable
文件。
SSTable
按照分層滾動合并的方式,Level DB
的Level 0
層最多只能保存4個SSTable
,當Level 0
層滿了之后,我們就將它們進行多路歸并,合并后的文件就是Level 1
層的有序SSTable
文件,
除Level 0
層,SSTable
層的key的范圍是不交叉的,這樣查詢的時候,就可以通過二分查找的方式進行快速查找了。Level 1
層的SSTable
就會從Level 1
層中輪詢的方式選擇一個SSTable
去和Level 2
層在此SSTable
的key
范圍內的SSTable
進行文件合并,為了防止合并的占用的IO比較多,所以在生成SSTable
的會判斷此文件和下一層多少個SSTable
有key
的重合,如果超過10個就停止寫入,生成新的SSTable
文件。這樣每次合并SSTable
文件消耗的IO也不至于太多。
合并的過程中,老的SSTable
文件,是不能刪除的,所以就會同時存在老的SSTable
文件和新的SSTable
文件,導致同一份數據因為被compaction
,數據最多可能膨脹到原來的2倍。
不過數據會膨脹,由于在compaction
過程中,有可能同一份數據不斷隨著compaction
過程向更高層重復寫入,有多少層有可能就寫入多少次,IO幾倍量的增加。
六 參考
《核心索引20講》 陳東
《數據密集型應用系統設計》 Martin Leppmann
七 詩詞欣賞
我寧愿瞄準星星,
卻擊不中他們;
也不愿沒有目標;
我寧愿去追逐目標,
卻得不到它,
也不愿不曾追逐,
我寧愿去嘗試卻失敗,
也不愿不曾嘗試。
我不想活著的每一天
都在幻想如果我當初付出了更多努力
現在會怎樣?
我會去放手追逐、
無論刀山火海,
我會去追逐我的命運!
你不能朝著你的夢想漫步,
你不能朝著夢想行走,
你要向它狂奔!