HBase——LSM-Tree

前言

LSM 樹,即日志結構合并樹(Log-Structured Merge-Tree)是Google BigTable 和 HBase 的基本存儲算法,它是傳統關系型數據庫的 B+ 數的改進。算法的關注重心是 “如何在頻繁的數據改動下保持系統讀取速度的穩定性”,算法的核心在于盡量保證數據是順序存儲到磁盤上的,并且會有頻繁地對數據進行整理,保證其順序性。而順序性就可以最大程度保證數據的讀取性能穩定。

在有代表性的關系型數據庫如MySQL、SQL Server、Oracle中,數據存儲與索引的基本結構就是我們耳熟能詳的B樹和B+樹。而在一些主流的NoSQL數據庫如HBase、Cassandra、LevelDB、RocksDB中,則是使用日志結構合并樹(Log-structured Merge Tree,LSM Tree)來組織數據。

一、B樹,B+樹,LSM樹

1.1 B樹

B樹,又叫做平衡多路查找樹。一個m階的b樹的特性如下:

  • 樹中的每個節點,最多有m個子節點。
  • 除了根節點之外,其他的每個節點至少有ceil(m/2)個子節點,ceil函數為取上限函數。
  • 所有的葉子節點都在同一層,葉子節點bubaohan任何關鍵信息。
  • 每個非葉子節點都包含有n個關鍵字信息:{n,a0,k1,a1,k2,……,kn,an},
    • n的取值范圍,[ceil(m/2)-1]<=n<=(m-1)
    • Ki(i=1...n)為關鍵字,且關鍵字的信息按照順序排序
    • Ai(i=0...n)為指向子節點的指針,且Ai指向的子樹節點的關鍵字信息必須大于ki,并且小于k(i+1)

上圖為一個3階的b樹,即m=3

  • 每個節點最多有3個子節點
  • 每個節點最少有ceil(m/2)=2個子節點
  • 每個節點至少有1<=n<=2個關鍵字信息

對于一棵節點為N階數為M的樹,查找和插入需要的比較次數為logM-1N~logM/1

1.2 B+樹

B+樹是b樹的一個變種,差別如下

  • 所有的葉子節點中包含了全部的關鍵字信息,以及指向含有這些關鍵詞信息記錄的指針
  • 葉子節點中的關鍵字信息是有序鏈接的
  • 非葉子節點相當于是葉子節點的索引,葉子節點相當于是存儲數據的數據層

1.3 LSM樹

LSM樹(log-Structured Merge-Trees)原理是將一棵大樹拆分成了多棵小樹,每棵小樹其實是一個有序的b+樹。數據寫入首先寫入到內存中,隨著小樹越來越大,小樹flush到磁盤中。磁盤中的小樹數量到達一定量后,對這些小樹做merge操作,合并成了一棵大的b+樹。LSM樹犧牲了部分讀性能(因為需要遍歷多棵小樹)來提高了寫性能。


二、核心思想

LSM樹核心就是放棄部分讀能力,換取寫入的最大化能力。LSM 樹會將所有的數據插入、修改、刪除等操作保存在內存中,當此類操作達到一定得數據量后,再批量地寫入磁盤當中。而在寫磁盤時,會和以前的數據做合并。在合并過程中,并不會像 B+ 樹一樣,在原數據的位置上修改,而是直接插入新的數據, 從而避免了隨機寫。

HBase的一個列簇(Column Family)本質上就是一棵LSM樹(Log-StructuredMerge-Tree)。LSM樹分為內存部分和磁盤部分。內存部分是一個維護有序數據集合的數據結構。一般來講,內存數據結構可以選擇平衡二叉樹、紅黑樹、跳躍表(SkipList)等維護有序集的數據結構,這里由于考慮并發性能,HBase選擇了表現更優秀的跳躍表。磁盤部分是由一個個獨立的文件組成,每一個文件又是由一個個數據塊組成。

LSM樹本質上和B+樹一樣,是一種磁盤數據的索引結構。但和B+樹不同的是,LSM樹的索引對寫入請求更友好。因為無論是何種寫入請求,LSM樹都會將寫入操作處理為一次順序寫,而HDFS擅長的正是順序寫(且HDFS不支持隨機寫),因此基于HDFS實現的HBase采用LSM樹作為索引是一種很合適的選擇。

LSM樹的索引一般由兩部分組成,一部分是內存部分,一部分是磁盤部分。內存部分一般采用跳躍表來維護一個有序的KeyValue集合。磁盤部分一般由多個內部KeyValue有序的文件組成。


2.1 KeyValue結構:

LSM樹中存儲的是多個keyValue組成的集合。每一個KeyValue由一個字節數組來表示。如下圖所示


總體來說,字節數組主要分為以下幾個字段。其中Rowkey、Family、Qualifier、Timestamp、Type這5個字段組成KeyValue中的key部分。
? keyLen:占用4字節,用來存儲KeyValue結構中Key所占用的字節長度。
? valueLen:占用4字節,用來存儲KeyValue結構中Value所占用的字節長度。
? rowkeyLen:占用2字節,用來存儲rowkey占用的字節長度。
? rowkeyBytes:占用rowkeyLen個字節,用來存儲rowkey的二進制內容。
? familyLen:占用1字節,用來存儲Family占用的字節長度。
? familyBytes:占用familyLen字節,用來存儲Family的二進制內容。
? qualif ierBytes:占用qualif ierLen個字節,用來存儲Qualif ier的二進制內

注意:HBase并沒有單獨分配字節用來存儲qualif ierLen,因為可以通過keyLen和其他字段的長度計算出qualif ierLen。代碼如下:


? timestamp:占用8字節,表示timestamp對應的long值。
? type:占用1字節,表示這個KeyValue操作的類型,HBase內有Put、Delete、Delete Column、DeleteFamily,等等。注意,這是一個非常關鍵的字段,表明了LSM樹內存儲的不只是數據,而是每一次操作記錄。

Value部分直接存儲這個KeyValue中Value的二進制內容。所以,字節數組串主要是Key部分的設計。

在比較這些KeyValue的大小順序時,HBase按照如下方式(偽代碼)來確定大小關系:


注意:在HBase中,timestamp越大的KeyValue,排序越靠前。因為用戶期望優先讀取到那些版本號更新的數據。

上面以HBase為例,分析了HBase的KeyValue結構設計。通常來說,在LSM樹的KeyValue中的Key部分,有3個字段必不可少:

  • Key的二進制內容。
  • 一個表示版本號的64位long值,在HBase中對應timestamp;這個版本號通常表示數據的寫入先后順序,版本號越大的數據,越優先被用戶讀取。甚至會設計一定的策略,將那些版本號較小的數據過期淘汰(HBase中有TTL策略)。
  • type,表示這個KeyValue是Put操作,還是Delete操作,或者是其他寫入操作。本質上,LSM樹中存放的并非數據本身,而是操作記錄。這對應了LSM樹(Log-Structured Merge-Tree)中Log的含義,即操作日志。

2.2 MemStore實現部分

HBase中對LSM樹的實現,是在內存中用一個ConcurrentSkipListMap保存數據,即MemStore。ConcurrentSkipListMap是利用跳躍表(SkipList)的數據結構實現的。


跳躍表是一種能高效實現插入、刪除、查找的內存數據結構,這些操作的期望復雜度都是O(logN)。

跳躍表的優勢在于

    1. 比紅黑樹或其他的二分查找樹的實現簡單很多 2.并發場景下加鎖粒度更小,提高并發性能。因此諸Redis、LevelDB、HBase等KV數據庫,都把跳躍表作為一種維護有序數據集合的核心數據結構。

跳躍表可以看作是一種特殊的有序鏈表。跳躍表是由多層有序鏈表組成。最底一層的鏈表保存了所有的數據,為了提高鏈表的查詢效率,通過每向上的一層鏈表依次保存下一層鏈表的部分數據作為索引,采用空間換取時間等方式提高效率。相鄰的兩層鏈表中元素相同的節點之間存在引用關系,一般是上層節點中存在一個指向下層節點的引用。

跳躍表的目的在于提高了查詢效率,同時也犧牲了一定的存儲空間,在跳躍表中查找一個指定元素的流程比較簡單。如上圖所示,以左上角元素作為起點:如果發現當前節點的后繼節點的值小于等于待查詢值,則沿著這條鏈表向后查詢,否則,切換到當前節點的下一層鏈表。繼續查詢,直到找到待查詢值為止(或者當前節點為空節點)為止。

跳躍表的構建稍微復雜一點,首先,需要按照上述查找流程找到待插入元素的前驅和后繼;然后,按照如下隨機算法生成一個高度值:

// p是一個(0,1)之間的常數,一般取p=1/4或者1/2
public void randomHeight(doubule p) {
    int height = 0;
    while(random.newtDouble < p) {
        height++;
    }
    return height + 1;
}

最后,將待插入節點按照randomHeight生成一個垂直節點的位置(這個節點的層數位置正好等于高度值),之后插入到跳躍表的多條鏈表中去。假設height=randomHeight§,這里需要分兩種情況討論:如果height大于跳躍表的高度,那么跳躍表的高度被提升為height,同時需要更新頭部節點和尾部節點的指針指向。如果height小于等于跳躍表的高度,那么需要更新待插入元素前驅和后繼的指針指向。

2.3 磁盤文件實現部分-用多路歸并實現LSM樹的文件合并

隨著寫入的增加,內存數據會不斷地刷新到磁盤上。最終磁盤上的數據文件會越來越多。所以LSM樹的實現實際上是將寫入操作全部轉化為了磁盤的順序寫入,提高了寫入性能。但是,這種設計是以犧牲一定的讀操作性能為代價的,因為讀取的時候,需要歸并多個文件來獲取滿足條件的KV,非常消耗磁盤IO,這樣讀性能會很差,所以hbase內部采用多路合并思想異步地進行compaction把多個小文件合并成一個大文件,以提高讀數據性能。

多路歸并思想

回顧一下多路歸并的算法思路,假設現在有K個文件,其中第i個文件內部存儲有Ni個正整數(這些整數在文件內按照從小到大的順序存儲),如何將K個有序文件合并成一個大的有序文件?

可以使用多路歸并算法進行實現。對每個文件設計一個指針,取出K個指針中數值最小的一個,然后把最小的那個指針后移,接著繼續找K個指針中數值最小的一個,繼續后移指針……直到N個文件全部讀完為止。如下圖示例所示:


具體實現上,可以用一個最小堆來維護K個指針,每次從堆中取最小值,開銷為logK,最多從堆中取sum(Ni)次元素。

為了優化讀取操作的性能,hbase會進行兩種類型的compact。

  • minor compact:即選中少數幾個Hfile,將他們多路歸并成一個文件。這種方式的有點是可以進行局部的Compact,通過少量的IO減少文件個數,提高讀取操作的性能。適合較高頻率的執行。但它的缺點是只合并了局部的數據,對于那些全局刪除操作,無法在合并過程中完全刪除。

  • major compact:將所有的HFile一次性多路歸并成一個文件。這種方式的好處是,合并之后只有一個文件,這樣讀取的性能肯定是最高的。但它的問題是合并所有的文件可能需要很長的時間并消耗大量的IO走遠,因此major compact不宜使用太頻繁,可以選擇周期性的執行,或者在集群空閑時手動執行。

2.4 布隆過濾器(Bloom Filter)

布隆過濾器(Bloom Filter)是由布隆(Burton Howard Bloom)在1970年提出的。它實際上是由一個很長的二進制向量和一系列隨機映射函數組成,布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率(假正例False positives,即Bloom Filter報告某一元素存在于某集合中,但是實際上該元素并不在集合中)和刪除困難,但是沒有識別錯誤的情形(即假反例False negatives,如果某個元素確實沒有在該集合中,那么Bloom Filter 是不會報告該元素存在于集合中的,所以不會漏報)。

HBase為了提升LSM結構下的隨機讀性能,還引入了布隆過濾器(建表語句中可以指定),對應HFile中的Bloom index block,其結構圖如下所示。


由于布隆過濾器只需占用極小的空間,便可給出 “可能存在” 和 “肯定不存在”的存在性判斷。因此可以提前過濾掉很多不必要的數據塊,從而節省了大量的磁盤IO。hbase的get操作就是通過運用低成本高效率的布隆過濾器來過濾大量無效數據塊的,從而節省了大量磁盤IO。

如果在表中設置了Bloomfilter,那么HBase會在生成StoreFile時包含一份bloomfilter結構的數據,稱其為MetaBlock;MetaBlock與DataBlock(真實的KeyValue數據)一起由LRUBlockCache維護。所以,開啟bloomfilter會有一定的存儲及內存cache開銷。

hbase中布隆過濾器的3中類型:

  • none:關閉布隆過濾器功能
  • row:按照rowkey計算布隆過濾器的二進制串并存儲。get查詢時,必須帶rowkey.
  • rowcol:按照rowkey+family+qualifier這3個字段拼出byte[]來計算布隆過濾器值并存儲。如果查詢時,get可以指定到這3個字段,則肯定可以通過布隆過濾器提高性能。

任何類型的get(基于rowkey或row+col)Bloom Filter的優化都能生效,關鍵是get的類型要匹配Bloom Filter的類型。基于row的scan是沒辦法走Bloom Filter的,因為Bloom Filter是需要事先知道過濾項的,對于順序scan是沒有事先辦法知道owkey的,而get是指明了rowkey所以可以用Bloom Filter。

在使用布隆過濾器時,需要注意兩個問題:

  • 1、什么時候應該使用布隆過濾器?根據上面的描述,布隆過濾器的主要作用,是幫助HBase跳過那些顯然不包括所查找數據的底層文件。那么,當所查找的數據均勻分布在所有文件中(當用戶定期更新所有行時,就可能導致這種情況),布隆過濾器的作用就微乎其微,反而浪費了存儲空間。相反,如果我們查找的數據只包含在少部分的文件中,就應該果斷使用布隆過濾器。

  • 2、應該選擇行級還是行加列級布隆過濾器?很顯然,行加列級因為粒度更細,占用的存儲空間也就越多。因此,如果用戶總是讀取整行的數據,行級布隆過濾器就夠用了。在可以選擇的情形下,盡可能使用行級布隆過濾器,因為它在額外的空間開銷和利用過濾存儲文件提升性能之間取得了更好的平衡。

通過布隆過濾器,HBase就能以少量的空間代價,換來在讀取數據時非常快速地確定是否存在某條數據,效率進一步提升。

三、LSM 樹的讀寫

LSM 樹讀寫架構圖

LSM樹的結構是橫跨內存和磁盤的,包含memtable、immutable memtable、SSTable等多個部分。

  • memtable
    顧名思義,memtable是在內存中的數據結構,用以保存最近的一些更新操作,當寫數據到memtable中時,會先通過WAL的方式備份到磁盤中,以防數據因為內存掉電而丟失。memtable可以使用跳躍表或者搜索樹等數據結構來組織數據以保持數據的有序性。當memtable達到一定的數據量后,memtable會轉化成為immutable memtable,同時會創建一個新的memtable來處理新的數據。

  • immutable memtable
    顧名思義,immutable memtable在內存中是不可修改的數據結構,它是將memtable轉變為SSTable的一種中間狀態。目的是為了在轉存過程中不阻塞寫操作。寫操作可以由新的memtable處理,而不用因為鎖住memtable而等待。

  • SSTable
    SSTable(Sorted String Table)即為有序鍵值對集合,是LSM樹組在磁盤中的數據的結構。如果SSTable比較大的時候,還可以根據鍵的值建立一個索引來加速SSTable的查詢。下圖是一個簡單的SSTable結構示意:

寫入操作

寫操作首先需要通過WAL將數據寫入到磁盤Log中,防止數據丟失,然后數據會被寫入到內存的memtable中,這樣一次寫操作即已經完成了,只需要1次磁盤IO,再加1次內存操作。相較于B+樹的多次磁盤隨機IO,大大提高了效率。隨后這些在memtable中的數據會被批量的合并到磁盤中的SSTable當中,將隨機寫變為了順序寫。

刪除操作

當有刪除操作時,并不需要像B+樹一樣,在磁盤中的找到相應的數據后再刪除,只需要在memtable中插入一條數據當作標志,如delKey:1933,當讀操作讀到memtable中的這個標志時,就會知道這個key已被刪除。隨后在日志合并中,這條被刪除的數據會在合并的過程中一起被刪除。

更新操作

更新操作和000刪除操作類似,都是只操作memtable,寫入一個標志,隨后真正的更新操作被延遲在合并時一并完成。

查詢操作

查詢操作相較于B+樹就會很慢了,讀操作需要依次讀取memtable、immutable memtable、SSTable0、SSTable1…。需要反序地遍歷所有的集合,又因為寫入順序和合并順序的緣故,序號小的集合中的數據一定會比序號大的集合中的數據新。所以在這個反序遍歷的過程中一旦匹配到了要讀取的數據,那么一定是最新的數據,只要返回該數據即可。但是如果一個數據的確不在所有的數據集合中,則會白白得遍歷一遍。
讀操作看上去比較笨拙,所幸可以通過布隆過濾器來加速讀操作。當布隆過濾器顯示相應的SSTable中沒有要讀取的數據時,就跳過該SSTable。

布隆過濾器實際上是一個很長的二進制向量和一系列隨機映射函數。布隆過濾器可以用于檢索一個元素是否在一個集合中。它的優點是空間效率和查詢時間都遠遠超過一般的算法,缺點是有一定的誤識別率和刪除困難。

還有上面提到的索引文件,也可以加速讀操作。

合并操作

由前面的增刪改查操作來看,合并操作是LSM樹最重要的操作。
合并操作有兩個主要的作用:
合并內存中的數據到磁盤中。
由于將內存數據合并到磁盤當中會產生大量的小的集合,并且更新和刪除操作會產生大量的冗余數據,通過合并操作可以減少集合中的冗余數據并降低讀操作時線性掃描的耗時。

參考:
https://blog.csdn.net/u011047968/article/details/125298135

https://www.cnblogs.com/swordfall/p/10567468.html

https://segmentfault.com/a/1190000023390528

https://blog.csdn.net/breakout_alex/article/details/111350371

https://www.shuzhiduo.com/A/E35pQ9rAdv/

https://www.cnblogs.com/importbigdata/p/14270069.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容