原文:
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.44.2782&rep=rep1&type=pdf
Google的BigTable架構在分布式結構化存儲方面大名鼎鼎,其中的MergeDump模型在讀寫之間找到了一個較好的平衡點,很好的解決了web scale數據的讀寫問題。
MergeDump的理論基礎是LSM-Tree (Log-Structured Merge-Tree)
思想
LSM的思想,在于對數據的修改增量保持在內存中,達到指定的限制后將這些修改操作批量寫入到磁盤中,相比較于寫入操作的高性能,讀取需要合并內存中最近修改的操作和磁盤中歷史的數據,即需要先看是否在內存中,若沒有命中,還要訪問磁盤文件。
原理:把一顆大樹拆分成N棵小樹,數據先寫入內存中,隨著小樹越來越大,內存的小樹會flush到磁盤中。磁盤中的樹定期做合并操作,合并成一棵大樹,以優化讀性能。
對應于使用LSM的Leveldb來說,對于一個寫操作,先寫入到memtable中,當memtable達到一定的限制后,這部分轉成immutable memtable(不可寫),當immutable memtable達到一定限制,將flush到磁盤中,即sstable.,sstable再進行compaction操作。
LSM思想非常樸素,就是將對數據的更改hold在內存中,達到指定的threadhold后將該批更改批量寫入到磁盤,在批量寫入的過程中跟已經存在的數據做rolling merge。
舉個例子
拿update舉個例子:
比如有1000萬行數據,現在希望update table.a set addr='new addr' where pk = '833',如果使用B-Tree類似的結構操作,就需要:
- 找到該條記錄所在的page
- load page到內存(如果恰好該page已經在內存中,則省略該步)
- 如果該page之前被修改過,則先flush page to disk
- 修改數據
上面的動作平均來說有兩次disk I/O,如果采用LSM-Tree類似結構,則:
- 將需要修改的數據直接寫入內存
可見這里是沒有disk I/O的。但是,這樣的話讀的時候就費勁了,需要merge disk上的數據和memory中的修改數據,這顯然降低了讀的性能。
確實如此,所以作者其中有個假設,就是寫入遠大于讀取的時候,LSM是個很好的選擇。我覺得更準確的描述應該是”優化了寫,沒有顯著降低讀“,因為大部分時候我們都是要求讀最新的數據,而最新的數據很可能還在內存里面,即使不在內存里面,只要不是那些更新特別頻繁的數據,其I/O次數也是有限的。
所以LSM-Tree比較適合的應用場景是:insert數據量大,讀數據量和update數據量不高且讀一般針對最新數據。
淺析
內存的高效性是lsm的結構基礎,我們在訪問數據的時候速度肯定是內存大于磁盤的,這樣的話為什么不全用內存呢?原因大家自己考慮下就行了,所以權衡下來還是需要用硬盤的,那么為了實現數據的快速插入和查詢,存儲應該怎么設計呢?
我們知道,要使一個表對查詢的響應比較快,那么最主要的手段就是索引,但是索引多了就會影響數據插入的速度,這也是一種平衡,下面我們將分析lsm,看看它是設計了個完美的解決方案嗎?
lsm tree解決了什么問題? 它解決了隨機讀寫創景下,減少數據頻繁的插入、修改、刪除操作所需要的磁盤I/O次數
學過數據結構的同學都知道Btree樹是很多索引的優先選擇結構,b tree樹訪問的時間復雜度接近Logm(N/2),可以計算下,在成百上千的索引節點下,即使索引十幾億的數據,那么樹的深度也不會很深的,應該是10以內吧,再加上對于lru算法的支持,可以很明顯的減少io,那為什么hbase不用這個結構呢,答案就是本文開頭的幾句話。
因為hbase中數據插入是比較隨機的或者說是無序的,在查詢數據的時候回到索引上,也就是對于某個葉子節點的訪問是很隨機的,這個場景很重要,那么我們根據這個具體場景分析一下b+樹,因為查詢是隨機的,那么也就是說我們上次調入內存的數據可能很久以后都不會被訪問,所以lru算法失去了它的價值,主要的系統開銷變成了訪問B+樹的io了,內存的命中率很低,對于插入數據來說道理是一樣的。
再看看lsm tree是怎么做的:
lsm構造許多小的結構,每個結構在內存里排序一下構成內部有序,查詢的時候對每個小結構就可以采用二分法高效的查找定位,我們都知道有序的東西查找起來速度肯定比無序的快,如果只是這么設計肯定不能達到快速插入和查詢的目的,lsm還引入了Bloom filter和小樹到大樹的排序。
Bloom Filter是一種空間效率很高的隨機數據結構,它利用位數組很簡潔地表示一個集合,通過多hash函數判斷一個元素是否屬于這個集合。 Bloom Filter的這種高效是有一定代價的:在判斷一個元素是否屬于某個集合時,有可能會把不屬于這個集合的元素誤認為屬于這個集合(false positive)。因此,Bloom Filter不適合那些“零錯誤”的應用場合。而在能容忍低錯誤率的應用場合下,Bloom Filter通過極少的錯誤換取了存儲空間的極大節省。
Bloom filter在lsm中的作用就是判斷要查詢的數據在哪個內存部件中,或者要插入的數據應該插入到哪個內存部件中。
小樹到大樹的排序是為了節約內存,同時也是為了恢復,因為我們知道hbase的delete和update其實都是insert,這都是由lsm的特點決定的,新的數據會被寫到磁盤新的位置上去,這樣就保證了舊記錄不會被覆蓋,在系統crash后的恢復過程會很有用,只要按日志順序恢復就ok了。
什么是lsm tree:
LSM-Tree通過使用一個基于內存的組件C0和一至多個基于磁盤的(C1, C2, …, CK)組件算法,對索引變更進行延遲及批量處理,并通過歸并的方式高效的將更新遷移到磁盤。(有點耐個)
原理
Write & Read
- 當一些更新操作到達時,他們會被寫到內存緩存(也就是memtable)中,memtable使用樹結構來保持key的有序
- 在大部 分的實現中,memtable會通過寫WAL的方式備份到磁盤,用來恢復數據,防止數據丟失。
- 當memtable數據達到一定規模時會被刷新到磁盤上的一 個新文件,重要的是系統只做了順序磁盤讀寫,因為沒有文件被編輯,新的內容或者修改只用簡單的生成新的文件。
- 所以越多的數據存儲到系統中,就會有越多的不可修改的,順序的sstable文件被創建,它們代表了小的,按時間順序的修改。
- 因為比較舊的文件不會被更新,重復的紀錄只會通過創建新的紀錄來覆蓋,這也就產生了一些冗余的數據。所以系統會周期的執行合并操作(compaction)。
- 合并操作選擇一些文件,并把他們合并到一起,移除重復的更新或者刪除紀錄,同時也會刪除上述的冗余。更重要的是,通過減少文件個數的增長,保證讀操作的性 能。因為sstable文件都是有序結構的,所以合并操作也是非常高效的。
- 當一個讀操作請求時,系統首先檢查內存數據(memtable),如果沒有找到這個key,就會逆序的一個一個檢查sstable文件,直到key 被找到。
- 因為每個sstable都是有序的,所以查找比較高效(O(logN)),但是讀操作會變的越來越慢隨著sstable的個數增加,因為每一個 sstable都要被檢查。(O(K log N), K為sstable個數, N 為sstable平均大小)。所以,讀操作比其它本地更新的結構慢。
- 幸運的是,有一些技巧可以提高性能。最基本的的方法就是頁緩存(也就是leveldb的 TableCache,將sstable按照LRU緩存在內存中)在內存中,減少二分查找的消耗。
- LevelDB 和 BigTable 是將** block-index 保存在文件尾部**,這樣查找就只要一次IO操作,如果block-index在內存中。一些其它的系統則實現了更復雜的索引方法。
- 即使有每個文件的索引,隨著文件個數增多,讀操作仍然很慢。通過周期的合并文件,來保持文件的個數,因些讀操作的性能在可接收的范圍內。
- 即便有了合 并操作,讀操作仍然會訪問大量的文件,大部分的實現通過Bloom filter來避免大量的讀文件操作,布隆過濾器是一種高效的方法來判斷一個sstable中是否包含一個特定的key。(如果bloom說一個key不存在,就一定不存在,而當bloom說一個文件存在是,可能是不存在的,只是通過概率來保證)所有的寫操作都被分批處理,只寫到順序塊上。
-
另外,合并操作的周期操作會對IO有影響,讀操作有可能會訪問大量的文件(散亂的讀)。這簡化了算法工作的方法,我們交換了讀和寫的隨機IO。這種折衷很有意義,我們可以通過軟件實現的技巧像布隆過濾器或者硬件(大文件cache)來優化讀性能
Basic Compaction
為了保持LSM的讀操作相對較快,維護并減少sstable文件的個數是很重要的,所以讓我們更深入的看一下合并操作。這個過程有一點兒像一般垃圾回收算法。
- 當一定數量的sstable文件被創建,例如有5個sstable,每一個有10行,他們被合并為一個50行的文件(或者更少的行數)。
- 這個過程一 直持續著,當更多的有10行的sstable文件被創建,當產生5個文件時,它們就被合并到50行的文件。
-
最終會有5個50行的文件,這時會將這5個50 行的文件合并成一個250行的文件。這個過程不停的創建更大的文件。像下圖:
Levelled Compaction
- Level 0可以認為是MemTable的文件映射內存, 因此每個Level 0的SSTable之間的key range可能會有重疊。其他Level的SSTable key range不存在重疊。
- Level 0的寫入是簡單的創建-->順序寫流程,因此理論上,寫磁盤的速度可以接近磁盤的理論速度。
更新的實現,像 LevelDB 和 Cassandra解決這個問題的方法是:
- 實現了一個分層的,而不是根據文件大小來執行合并操作。
- 這個方法可以減少在最壞情況下需要檢索的文件個數,同時也減少了一次合并操作的影響。
按層合并的策略相對于上述的按文件大小合并的策略有二個關鍵的不同:
- 每一層可以維護指定的文件個數,同時保證不讓key重疊。也就是說把key分區到不同的文件。因此在一層查找一個key,只用查找一個文件。
- 第一層是特殊情況,不滿足上述條件,key可以分布在多個文件中。
- 每次,文件只會被合并到上一層的一個文件。當一層的文件數滿足特定個數時,一個文件會被選出并合并到上一層。
-
這明顯不同與另一種合并方式:一些相近大小的文件被合并為一個大文件。
SSTable合并類似于簡單的歸并排序:根據key值確定要merge的文件,然后進行合并。因此,合并一個文件到更高層,可能會需要寫多個文件。存在一定程度的寫放大。
這些改變表明按層合并的策略減小了合并操作的影響,同時減少了空間需求。除此之外,它也有更好的讀性能。但是對于大多數場景,總體的IO次數變的更多,一些更簡單的寫場景不適用。
小結
- LSM 是日志和傳統的單文件索引(B+ tree,Hash Index)的中立,他提供一個機制來管理更小的獨立的索引文件(sstable)。
- 通過管理一組索引文件而不是單一的索引文件,LSM 將B+樹等結構昂貴的隨機IO變的更快,而代價就是讀操作要處理大量的索引文件(sstable)而不是一個,另外還是一些IO被合并操作消耗
Ref:
https://www.zhihu.com/question/19887265
http://f.dataguru.cn/thread-24939-1-1.html