HBase表模式的設計
對於HBase表,在設計表結構之前,我們需要先考慮的幾個問題:
- 這個表應該有多少個列族?
- 列族使用的是什么數據?
- 每個列族應該有多少列?
- 列名應該是什么?盡管列名不必在建表的時候定義,但是后期讀寫數據時是需要知道的。
- 單元存放什么數據?
- 每個單元存儲多少個時間版本?
- 行健結構是什么?應該包含什么信息?
模式影響到表結構和如何讀寫表,所以說把這些放到寬泛的模式設計中變得尤為重要。
一、HBase的存儲方式
HBase底層物理存儲是基於HDFS,在HDFS上是以HFile的形式進行存儲的:
- 表中的列族在HDFS上是以HFile的形式存在,一個HFile對應一個列族,但是一個列族可能會對應多個HFile。
一個特定的列族的所有數據在HDFS上會有一個物理存儲,這個物理存儲區可能會有多個HFile組成,理論上可以通過合並來得到一個HFile,一個列族的所有列在硬盤上是存放在一起的,使用這個特性,可以把不同模式的列放在不同的列族,以便隔離他們。這也是HBase面相列族存儲的原因。
二、寬表與高表
- 寬表:
HBase中所謂的寬表,指的是表中行少而列多,也就是說一行當中包含有很多的列,但是表整體行很少,比如一張表中行健一共有100個,但是每個行健所包含的列有1000個,這種就是所謂的寬表。
- 高表
HBase中所謂的高表,恰好與寬表相反,行多,列少。比如一張表中行有100萬條,而每行對應的列卻又10個,這既是所謂的高表。
三、HBase的訪問時間復雜度
為了便於描述,先定義以下變量:
- n = 表中KeyValue條目數量(包括Put的結果和Delete留下的墓碑標記)
- b = HFile中數據塊的數量(HFile Block)
- e = 平均一個HFile中一個KeyValue條目的數量(如果知道行的大小,可以進行計算出來)
- c = 每行中列的平均數量
先定義針對指定行健查找相關HFile數據塊需要的時間。無論是在單行上執行get(),還是為一次掃描查找起始鍵,都會有這個動作。
第一步、客戶端需要先找到正確的HRegionServer和region。需要花費3次固定運算找到正確的region,1、查找zk,2、查找-ROOT-,3、查找.META.這是一次O(1)的運算。
在指定region上,行在讀的過程中可能存在於兩個地方,如果還沒有刷寫到磁盤就位於memStore,如果已經刷鞋,則位於一個HFile中。假定只有一個HFile,這一行要么在文件中,要么還沒刷寫,在MemStore中。
如果用e代表在任意合理的時間在MemStore的條目數量。
如果一行在MemStore中,因為MemStore是用跳表實現的。所以查找行的時間復雜度是O(log e)。
如果一行已經刷到磁盤上了,那么就需要先找到正確的HFile數據塊。數據塊索引是排過序的,所以查找正確的數據塊是一次時間復雜度為O(log b)的運算。查找行里的KeyValue對象是在數據塊里的一次線性掃描操作。在找到第一個KeyValue對象之后,隨后查找剩下的對象就是一次線性掃描。
假設行里的單元都在同一個數據塊中,掃描的時間復雜度是O(e/b)。
如果行里的單元不在同一個數據塊中,這種掃描需要訪問多個連續的數據塊里的數據,所以這時的運算有讀取的行數決定,時間復雜度是O(c)。
也就是說這種掃描的時間復雜度是O(Max(c,e/b))
所以在訪問HBase中的數據時,決定性因素是掃描HFile數據塊找到相關KeyValue對象所花費的時間。如果是寬行的話,掃描過程中會增加處理整行的開銷。以上的這些分析都是基於在知道行健的情況下。
如果不知道行健的話,就需要掃描整個區間(有可能是整張表)來查找你關心的行,而這個時間復雜度是O(n)
這里沒有討論關於硬盤尋道的開銷。如果需要從HFile里讀取的數據已經被加載進數據緩存中,前面的分析是正確的。因為行健是所有這些索引的決定性因素,所以結論是:訪問寬行要比訪問窄行開銷大。
四、rowkey的散列
HBase的rowkey在設計的時候我們一般都要對其進行散列處理,這樣做有以下幾個好處:
- 可以得到定長的行健,即行健的長度是同一的,可以更好的預測讀寫性能
- 可以在掃描表的時候可以方便的設置起始域停止鍵
- 有助於數據更均勻的分部在region上。減輕或避免發生熱點問題
熱點:負載極度集中在一小部分的region上。因為負載沒有分散的整個集群上,這是不合理的。服務這些region的幾台機器承擔了絕大部分的工作,將成為整體性能的拼勁。
對rowkey散列,雖然在所有region上實現了一個均勻的分部,但是這樣的話就會失去數據的順序。換句話說,就是不能在掃描一個小的時間范圍。要么掃描整個表(scan),要么獲取指定的行(get()或get(List))
散列和MD5:
散列函數是把編程的巨長數值映射到定長的小數值上的一種函數。散列算法有很多種,MD5就是其中的一種,也是比較常用的一種。MD5對任何數據進行散列運算生成一個128位(16字節)的散列值。這是一種流行的散列函數,通常生產中,我是對rowkey進行16位的MD5操作。
五、目標數據的訪問(重點:索引)
之前談到關於HBase的高表與寬表,高表有利於快速得到正確的行。而寬表則有利於進行批量寫操作。
對於HBase中的索引來說,只有鍵才可以建立索引(KeyValue對象的key部分,包括行健,列限定符和時間戳),可以將其看作是關系型數據庫的主鍵,但是不能夠改變構成主鍵的列,這里的鍵是由3個元素復合而成的(行健,列限定符和時間戳)。
在列限定符和時間戳上建立索引,可以在一行上不用掃描前面所有的列而直接跳到正確的列。取回的KeyValue對象基本上來自於HFile的一行。
從表中獲取數據的方式有兩種,get和scan。如果只是需要獲取指定的某行或這個某幾行,也就是說在明確知道行健的情況下,建議使用get或get<List>的方式進行獲取。
如果知道起始鍵和停止鍵,則可以使用scan的方式來限制掃描器掃描的的行數進行獲取數據。
在get對象中,可以指定列族和列限定符。當指定列族之后,可以限制客戶端只訪問指定列族的HFile;當時指定列限定符之后不會限制從硬盤中讀出的HFile,只是可以限制網絡上傳回給客戶端的東西。如果給定的region上一個列族存在多個HFile。要查找get()調用里指定的行的內容,不管有多少個HFile包含與請求有關的數據,都要訪問所有的HFile。但是get里盡可能的明確查找內容是有必要的,因為不必在網絡上傳回客戶端不需要的數據。唯一的開銷也就是RegionServer上可能的硬盤IO,同樣,如果get中指定時間戳的話,可以避免讀取早於時間戳的HFile。
六、小結
- HBase表很靈活,可以用字符數組形式存儲任何東西
- 同一列族中存儲相似訪問模式的東西
- 索引是建立在KeyValue對象的Key部分上,Key是由行健,列限定符合時間戳按次序組成
- 高表可能支持將運算復雜度降到O(1),但是要在原子性上付出代價
- 設計HBase模式的時候進行范規范化處理是一種可行的辦法
- HBase不支持跨行事務,要避免在代碼中維護這種復雜的邏輯
- 散列支持定長鍵和更均勻的數據分部,但是失去了排序的好處
- 列限定符可以用來存儲數據,就像單元一樣
- 可以將數據放入到列限定符中,所以它的長度會影響到存儲空間,當訪問數據時,也影響了硬盤和網絡IO的開銷,所以盡量簡練
- 列族名字的長度會影響通過網絡傳回客戶端的數據大小,所以,也盡量簡練