LSM-tree 基本原理及應用

LSM-tree 在 NoSQL 系統裏很是常見,基本已經成爲必選方案了。今天介紹一下 LSM-tree 的主要思想,再舉一個 LevelDB 的例子。數據庫

正文 3056 字,預計閱讀時間 8 分鐘。架構

LSM-tree異步

起源於 1996 年的一篇論文《The Log-Structured Merge-Tree (LSM-Tree)》,這篇論文 32 頁,我一直沒讀,對 LSM 的學習基本都來自頂會論文的背景知識以及開源系統文檔。今天的內容和圖片主要來源於 FAST'16 的《WiscKey: Separating Keys from Values in SSD-conscious Storage》。分佈式

先看名字,log-structured,日誌結構的,日誌是軟件系統打出來的,就跟人寫日記同樣,一頁一頁往下寫,並且系統寫日誌不會寫錯,因此不須要更改,只須要在後邊追加就行了。各類數據庫的寫前日誌也是追加型的,所以日誌結構的基本就指代追加。注意他仍是個 「Merge-tree」,也就是「合併-樹」,合併就是把多個合成一個。學習

好,不扯淡了,說正文了。設計

LSM-tree 是專門爲 key-value 存儲系統設計的,key-value 類型的存儲系統最主要的就兩個個功能,put(k,v):寫入一個(k,v),get(k):給定一個 k 查找 v。日誌

LSM-tree 最大的特色就是寫入速度快,主要利用了磁盤的順序寫,pk掉了須要隨機寫入的 B-tree。關於磁盤的順序和隨機寫能夠參考:《blog

硬盤的各類概念排序

索引

下圖是 LSM-tree 的組成部分,是一個多層結構,就更一個樹同樣,上小下大。首先是內存的 C0 層,保存了全部最近寫入的 (k,v),這個內存結構是有序的,而且能夠隨時原地更新,同時支持隨時查詢。剩下的 C1 到 Ck 層都在磁盤上,每一層都是一個在 key 上有序的結構。

 

寫入流程:一個 put(k,v) 操做來了,首先追加到寫前日誌(Write Ahead Log,也就是真正寫入以前記錄的日誌)中,接下來加到 C0 層。當 C0 層的數據達到必定大小,就把 C0 層 和 C1 層合併,相似歸併排序,這個過程就是Compaction(合併)。合併出來的新的 new-C1 會順序寫磁盤,替換掉原來的 old-C1。當 C1 層達到必定大小,會繼續和下層合併。合併以後全部舊文件均可以刪掉,留下新的。

注意數據的寫入可能重複,新版本須要覆蓋老版本。什麼叫新版本,我先寫(a=1),再寫(a=233),233 就是新版本了。假如 a 老版本已經到 Ck 層了,這時候 C0 層來了個新版本,這個時候不會去管底下的文件有沒有老版本,老版本的清理是在合併的時候作的。

寫入過程基本只用到了內存結構,Compaction 能夠後臺異步完成,不阻塞寫入。

查詢流程:在寫入流程中能夠看到,最新的數據在 C0 層,最老的數據在 Ck 層,因此查詢也是先查 C0 層,若是沒有要查的 k,再查 C1,逐層查。

一次查詢可能須要屢次單點查詢,稍微慢一些。因此 LSM-tree 主要針對的場景是寫密集、少許查詢的場景。

LSM-tree 被用在各類鍵值數據庫中,如 LevelDB,RocksDB,還有分佈式行式存儲數據庫 Cassandra 也用了 LSM-tree 的存儲架構。

LevelDB

其實光看上邊這個模型還有點問題,好比將 C0 跟 C1 合併以後,新的寫入怎麼辦?另外,每次都要將 C0 跟 C1 合併,這個後臺整理也很麻煩啊。這裏以 LevelDB 爲例,看一下實際系統是怎麼利用 LSM-tree 的思想的。

下邊這個圖是 LevelDB 的架構,首先,LSM-tree 被分紅三種文件,第一種是內存中的兩個 memtable,一個是正常的接收寫入請求的 memtable,一個是不可修改的immutable memtable。

 

另一部分是磁盤上的 SStable (Sorted String Table),有序字符串表,這個有序的字符串就是數據的 key。SStable 一共有七層(L0 到 L6)。下一層的總大小限制是上一層的 10 倍。

寫入流程:首先將寫入操做加到寫前日誌中,接下來把數據寫到 memtable中,當 memtable 滿了,就將這個 memtable 切換爲不可更改的 immutable memtable,並新開一個 memtable 接收新的寫入請求。而這個 immutable memtable 就能夠刷磁盤了。這裏刷磁盤是直接刷成 L0 層的 SSTable 文件,並不直接跟 L0 層的文件合併。

每一層的全部文件總大小是有限制的,每下一層大十倍。一旦某一層的總大小超過閾值了,就選擇一個文件和下一層的文件合併。就像玩 2048 同樣,每次能觸發合併都會觸發,這在 2048 裏是最爽的,可是在系統裏是挺麻煩的事,由於須要倒騰的數據多,可是也不是壞事,由於這樣能夠加速查詢。

這裏注意,全部下一層被影響到的文件都會參與 Compaction。合併以後,保證 L1 到 L6 層的每一層的數據都是在 key 上全局有序的。而 L0 層是能夠有重疊的。

 

上圖是個例子,一個 immutable memtable 刷到 L0 層後,觸發 L0 和 L1 的合併,假如黃色的文件是涉及本次合併的,合併後,L0 層的就被刪掉了,L1 層的就更新了,L1 層仍是全局有序的,三個文件的數據順序是 abcdef。

雖然 L0 層的多個文件在同一層,但也是有前後關係的,後面的同個 key 的數據也會覆蓋前面的。這裏怎麼區分呢?爲每一個key-value加個版本號。因此在 Compaction 時候應該只會留下最新的版本。

查詢流程:先查memtable,再查 immutable memtable,而後查 L0 層的全部文件,最後一層一層往下查。

LSM-tree讀寫放大

讀寫放大(read and write amplification)是 LSM-tree 的主要問題,這麼定義的:讀寫放大 = 磁盤上實際讀寫的數據量 / 用戶須要的數據量。注意是和磁盤交互的數據量纔算,這份數據在內存裏計算了多少次是不關心的。好比用戶原本要寫 1KB 數據,結果你在內存裏計算了1個小時,最後往磁盤寫了 10KB 的數據,寫放大就是 10,讀也相似。

寫放大:咱們以 RocksDB 的 Level Style Compaction 機制爲例,這種合併機制每次拿上一層的全部文件和下一層合併,下一層大小是上一層的 r 倍。這樣單次合併的寫放大就是 r 倍,這裏是 r 倍仍是 r+1 倍跟具體實現有關,咱們舉個例子。

假如如今有三層,文件大小分別是:9,90,900,r=10。又寫了個 1,這時候就會不斷合併,1+9=10,10+90=100,100+900=1000。總共寫了 10+100+1000。按理來講寫放大應該爲 1110/1,可是各類論文裏不是這麼說的,論文裏說的是等號右邊的比上加號左邊的和,也就是10/1 + 100/10 + 1000/100 = 30 = r * level。我的感受寫放大是一個過程,用一個數字衡量不太準確,並且這也只是最壞狀況。

讀放大:爲了查詢一個 1KB 的數據。最壞須要讀 L0 層的 8 個文件,再讀 L1 到 L6 的每個文件,一共 14 個文件。而每個文件內部須要讀 16KB 的索引,4KB的布隆過濾器,4KB的數據塊(看不懂不重要,只要知道從一個SSTable裏查一個key,須要讀這麼多東西就能夠了)。一共 24*14/1=336倍。key-value 越小讀放大越大。

總結

關於 LSM-tree 的內容和 LevelDB 的設計思想就介紹完了,主要包括寫前日誌 WAL,memtable,SStable 三個部分。逐層合併,逐層查找。LSM-tree 的主要劣勢是讀寫放大,關於讀寫放大能夠經過一些其餘策略去下降。

相關文章
相關標籤/搜索