《Hbase原理與實踐》讀書筆記——2.基礎資料結構與演算法

GokusJQK發表於2020-10-22

2.1 總體介紹

HBase的一個列簇(Column Family)本質上就是一棵LSM樹(Log-StructuredMerge-Tree)。LSM樹分為記憶體部分和磁碟部分。
記憶體部分是一個維護有序資料集合的資料結構。一般來講,記憶體資料結構可以選擇平衡二叉樹、紅黑樹、跳躍表(SkipList)等維護有序集的資料結構,這裡由於考慮併發效能,HBase選擇了表現更優秀的跳躍表。
磁碟部分是由一個個獨立的檔案組成,每一個檔案又是由一個個資料塊組成。對於資料儲存在磁碟上的資料庫系統來說,磁碟尋道以及資料讀取都是非常耗時的操作(簡稱IO耗時)。因此,為了避免不必要的IO耗時,可以在磁碟中儲存一些額外的二進位制資料,這些資料用來判斷對於給定的key是否有可能儲存在這個資料塊中,這個資料結構稱為布隆過濾器(Bloom Filter)。


2.2 跳躍表

2.2.1 跳躍表簡介

跳躍表(SkipList)是一種能高效實現插入、刪除、查詢的記憶體資料結構,這些操作的期望複雜度都是O(logN)。與紅黑樹以及其他的二分查詢樹相比,跳躍表的優勢在於實現簡單,而且在併發場景下加鎖粒度更小,從而可以實現更高的併發性。正因為這些優點,跳躍表廣泛使用於KV資料庫中,諸如Redis、LevelDB、HBase都把跳躍表作為一種維護有序資料集合的基礎資料結構。


2.2.2 跳躍表和連結串列

眾所周知,在維護有序資料集合上,大家的第一反應可能是連結串列,在已經找到待操作的節點的情況下,連結串列的插入和刪除的時間複雜度只有O(1),但是查詢的時間複雜度卻是O(n),需要逐個查詢,而跳錶正是為了優化連結串列的查詢複雜度而被提出,它在連結串列之上額外儲存了一些節點的索引資訊達到避免依次查詢元素的目的,從而將查詢複雜度優化為O(logN)。


2.2.3 跳躍表定義

•跳躍表由多條分層的連結串列組成(設為S0, S1, S2, … , Sn),例如圖中有6條連結串列。
•每條連結串列中的元素都是有序的。
•每條連結串列都有兩個元素:+∞(正無窮大)和- ∞(負無窮大),分別表示連結串列的頭部和尾部。
•從上到下,上層連結串列元素集合是下層連結串列元素集合的子集,即S1是S0的子集,S2是S1的子集。
•跳躍表的高度定義為水平連結串列的層數。



在這裡插入圖片描述


2.2.4 跳躍表查詢流程

•以左上角元素(設為currentNode)作為起點
•如果發現currentNode後繼節點的值小於等於待查詢值,則沿著這條連結串列向後查詢,否則,切換到當前節點的下一層連結串列。
•繼續查詢,直到找到待查詢值為止(或者currentNode為空節點)為止。

圖片表示查詢元素5的流程



在這裡插入圖片描述


2.2.5 跳躍表插入流程

1、需要按照上述查詢流程找到待插入元素的前驅和後繼。
2、按照拋硬幣演算法生成一個高度值height(比如以1/2的概率決定高度是否+1,如果成功+1繼續上述概率事件,如果失敗,則高度確定下來)。
3、將待插入節點按照高度值height生成一個垂直節點(這個節點的層數正好等於高度值),之後插入到跳躍表的多條連結串列中去,這裡有兩種情況(即height > 跳躍表高度 和 height <= 跳躍表高度)
•如果heigh > 跳躍表的高度,那麼跳躍表的高度被提升為height,同時需要更新頭部節點和尾部節點的指標指向。
•如果height <= 跳躍表的高度,那麼需要更新待插入元素前驅和後繼的指標指向。

圖片表示插入元素48的流程


在這裡插入圖片描述


2.2.6 跳躍表的性質

1、一個節點落在第k層的概率為p^(k-1)。
2、跳躍表的空間複雜度為O(n)。
3、跳躍表的高度為O(logn)。
4、跳躍表的查詢時間複雜度為O(logN)。
5、跳躍表的插入/刪除時間複雜度為O(logN)。


2.3 LSM樹

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

LSM樹的索引一般由兩部分組成,一部分是記憶體部分,一部分是磁碟部分。記憶體部分一般採用跳躍表來維護一個有序的KeyValue集合。磁碟部分一般由多個內部KeyValue有序的檔案組成。


2.3.1.KeyValue儲存格式

一般來說,LSM中儲存的是多個KeyValue組成的集合,每一個KeyValue一般都會用一個位元組陣列來表示,Hbase的位元組陣列設計如下


在這裡插入圖片描述

其中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的二進位制內容。
• qualifierBytes:佔用qualifierLen個位元組,用來儲存Qualifier的二進位制內容。注意,HBase並沒有單獨分配位元組用來儲存qualifierLen,因為可以通過keyLen和其他欄位的長度計算出qualifierLen:qualifierLen = keyLen - 2B -rowkeyLen - 1B - familyLen -8B - 1B
• timestamp:佔用8位元組,表示timestamp對應的long值。
• type:佔用1位元組,表示這個KeyValue操作的型別,HBase內有Put、Delete、Delete Column、DeleteFamily,表明了LSM樹記憶體儲的不只是資料,而是每一次操作記錄。

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


2.3.2 LSM樹的索引結構

一個LSM樹的索引主要由兩部分構成:記憶體部分和磁碟部分。記憶體部分是一個ConcurrentSkipListMap,Key就是前面所說的Key部分,Value是一個位元組陣列。資料寫入時,直接寫入MemStore中。隨著不斷寫入,一旦記憶體佔用超過一定的閾值時,就把記憶體部分的資料匯出,形成一個有序的資料檔案,儲存在磁碟上。

LSM樹索引結構如圖。記憶體部分匯出形成一個有序資料檔案的過程稱為flush。為了避免flush影響寫入效能,會先把當前寫入的MemStore設為Snapshot,不再容許新的寫入操作寫入這個Snapshot的MemStore。另開一個記憶體空間作為MemStore,讓後面的資料寫入。一旦Snapshot的MemStore寫入完畢,對應記憶體空間就可以釋放。這樣,就可以通過兩個MemStore來實現穩定的寫入效能。


在這裡插入圖片描述

隨著寫入的增加,記憶體資料會不斷地重新整理到磁碟上。最終磁碟上的資料檔案會越來越多。如果資料沒有任何的讀取操作,磁碟上產生很多的資料檔案對寫入並無影響,而且這時寫入速度是最快的,因為所有IO都是順序IO。但是,一旦使用者有讀取請求,則需要將大量的磁碟檔案進行多路歸併,之後才能讀取到所需的資料。因為需要將那些Key相同的資料全域性綜合起來,最終選擇出合適的版本返回給使用者,所以磁碟檔案數量越多,在讀取的時候隨機讀取的次數也會越多,從而影響讀取操作的效能。

優化讀取操作的效能,可以設定一定策略將選中的多個hfile進行多路歸併,合併成一個檔案。檔案個數越少,則讀取資料時需要seek操作的次數越少,讀取效能則越好。
1、major compact:將所有的hfile一次性多路歸併成一個檔案
優勢:合併之後只有一個檔案,這樣讀取的效能肯定是最高的。
劣勢:合併所有的檔案可能需要很長的時間並消耗大量的IO頻寬,因此,major compact不宜使用太頻繁,適合週期性地跑。

2、minor compact:選中少數幾個hfile,將它們多路歸併成一個檔案
優勢:進行區域性的compact,通過少量的IO減少檔案個數,提升讀取操作的效能,適合較高頻率地跑。
劣勢:只合並了區域性的資料,對於那些全域性刪除操作,無法在合併過程中完全刪除。因此,minor compact雖然能減少檔案,但卻無法徹底清除那些delete操作。而major compact能完全清理那些delete操作,保證資料的最小化。


2.3.3 總結

LSM樹的索引結構本質是將寫入操作全部轉化成磁碟的順序寫入,極大地提高了寫入操作的效能。但是,這種設計對讀取操作是非常不利的,因為需要在讀取的過程中,通過歸併所有檔案來讀取所對應的KV,這是非常消耗IO資源的。因此,在HBase中設計了非同步的compaction來降低檔案個數,達到提高讀取效能的目的。由於HDFS只支援檔案的順序寫,不支援檔案的隨機寫,而且HDFS擅長的場景是大檔案儲存而非小檔案,所以上層HBase選擇LSM樹這種索引結構是最合適的。


2.4 布隆過濾器

2.4.1 布隆過濾器案例說明

如何高效判斷元素w是否存在於集合A之中?
1、雜湊表 (解決小資料量場景下元素存在性判定,但如果A中元素數量巨大,甚至資料量遠遠超過機器記憶體空間,則無能為力)
2、基於磁碟和記憶體的雜湊索引 (覆蓋大資料量場景但實現成本不低)
3、布隆過濾器 (覆蓋大資料量場景,且低成本)

布隆過濾器由一個長度為N的01陣列array組成。首先將陣列array每個元素初始設為0。對集合A中的每個元素w,做K次雜湊,第i次雜湊值對N取模得到一個index(i),即index(i)=HASH_i(w)%N,將array陣列中的array[index(i)]置為1。最終array變成一個某些元素為1的01陣列。
下面舉個例子,如圖所示,A={x, y, z},N=18,K=3。


在這裡插入圖片描述

x,y,z三個元素迭代hash取模後,最終得到的布隆過濾器串為:010111000001010010。
但對於元素w,迭代hash取模後的結果下標分別是4、13、15,其中布隆過濾器的第15位為0,因此可以確認w肯定不在集合A中。

布隆過濾器串對任意給定元素w,給出的存在性結果為兩種:
•w可能存在於集合A中。(當hash迭代取模的下標的元素全為1)
•w肯定不在集合A中。 (當hash迭代取模的下標的元素存在為0)
這說明布隆過濾器存在誤判率(所謂誤判率也就是過濾器判定元素可能在集合中但實際不在集合中的佔比),他只能證明某個元素一定不在集合中,但不能肯定某個元素在集合中。

有論文中證明,當N取K*|A|/ln2時(其中|A|表示集合A元素個數),能保證最佳的誤判率。舉例來說,若集合有20個元素,K取3時,則設計一個N=3×20/ln2=87二進位制串來儲存布隆過濾器比較合適。


2.4.2 布隆過濾器與Hbase

由於布隆過濾器只需佔用極小的空間,便可給出“可能存在”和“肯定不存在”的存在性判斷。HBase的Get操作就是通過運用低成本高效率的布隆過濾器來過濾大量無效資料塊的,從而節省大量磁碟IO。

在HBase 1.x版本中,使用者可以對某些列設定不同型別的布隆過濾器,共有3種型別。
NONE:關閉布隆過濾器功能。
ROW:按照rowkey來計算布隆過濾器的二進位制串並儲存。Get查詢的時候,必須帶rowkey,所以使用者可以在建表時預設把布隆過濾器設定為ROW型別。
ROWCOL:按照rowkey+family+qualifier這3個欄位拼出byte[]來計算布隆過濾器值並儲存。如果在查詢的時候,Get能指定rowkey、family、qualifier這3個欄位,則可以通過布隆過濾器提升效能

注意:一般意義上的Scan操作,HBase都沒法使用布隆過濾器來提升掃描資料效能,但對於ROWCOL型別的布隆過濾器來說,如果在Scan操作中明確指定需要掃某些列,則同樣可以藉助布隆過濾器提升效能,如下所示:


在這裡插入圖片描述

Scan過程中,碰到KV資料從一行換到新的一行時,是沒法走ROWCOL型別布隆過濾器的,因為新一行的key值不確定;但是,如果在同一行資料內切換列時,則能通過ROWCOL型別布隆過濾器進行優化,因為rowkey確定,同時column也已知,也就是說,布隆過濾器中的Key確定,所以可以通過ROWCOL優化效能。

相關文章