HBase數據模型與KeyValue格式解析


HBase的核心存儲結構是KeyValue類。這個類定義了HBase的數據模型,並貫穿了HBase的整個讀寫鏈路。同時,HBase自身的元數據管理也是使用了業務表相同的模式。所以,從底層了解KeyValue的格式和設計,會加深我們對HBase基礎架構的理解,從而更好的使用和管理HBase。

數據模型淺析

HBase的數據模型是一個松散表結構,所謂松散,包含兩個方面的含義

  • 沒有schema:沒有一個地方定義了一行應該包括哪些列,這些列都是什么類型。這個信息通常只有用戶自己知道。
  • 稀疏:每行的列都可以完全不同,行與行之間的列在HBase層面沒有任何關聯

所以,我們說HBase是schema-free的,可以任意添加列。這些能力的基礎就是KeyValue的設計。

KeyValue對使用者而言是一個六元組,即(rowkey, family, qualifier, timestamp, type, value)。在1.x版本之后,添加了tags支持,變成了7元組,即(rowkey, family, qualifier, timestamp, type, value, tags)。但其設計思想是沒有變的,即key-value的方式進行存儲,從業務邏輯上看,key就是rowkey;value除了值本身,還包含了value的一些描述信息,即family、qualifier、timestamp和type。

所以,KeyValue本身在可以獨立的描述一行中的一列數據。因為帶上了列名信息,所以,不需要事先定義好一行有哪些列(schema)。也因為如此,一行中可以存在任意的列,每行的列都可以完全不同。這個能力相比傳統的RDBMS而言無疑是非常強大的,目前諸多的NoSQL系統幾乎都提供這種schema-free的能力。這個能力比較常見的應用可以是:

  • 從容應對業務模型變化:設計數據庫的人都知道,業務需求變了,表設計也要跟着變,經常要添加列。而HBase這種模型,不存在“添加列”這個操作,直接寫新列就好了。
  • 列名本身也可以存儲信息:因為列名本身與列值綁定在一起了,我們可以利用列名來存儲信息,比如
    • 時序場景,可以用列名作為數據的時間
    • 圖數據庫場景,可以用列名來描述“邊”

 

天下沒有免費的午餐,在獲得上述強大能力的同時,要付出的代價也是巨大的,即數據冗余,包括:

  • rowkey重復存儲:一行由多個具有相同rowkey的KeyValue組成
  • family,qualifier重復存儲

如果表是直接從RDBMS遷移過來的,每行都有相同的列,那無疑列名的重復會額外占用很多空間,尤其是一行中列較多的時候。這也是為什么在表設計時,要選取盡可能短小的family名字和列名。另外,rowkey的重復也有同樣的問題。我們有一些技術可以有效的解決這些問題。

  • DIFF壓縮:解決rowkey重復存儲的問題,在一行中列較多時效果非常明顯,這里不展開。
  • 列名映射:通常列名都是一些比較長的單詞或者短語,每列的列名不同,對DIFF壓縮不友好。所以,可以將易讀的列名映射為二進制的短列名(如short類型),HBase層面實際存儲的是1,2,3這樣的列名,而業務層通過一套列名映射機制在讀寫數據的時候進行列名轉換。用時間換空間。具體可以參考Phoenix的Column Name Econding(PHOENIX-1598)

 

下面,我們來看一下KeyValue的數據格式。

KeyValue格式(0.94)

 KeyValue本身就是一串二進制數據,即byte[],通過一些編碼規則,將二進制數據映射為六元組或七元組。下面,我們先看看094版本的KeyValue格式。

 

 0.94版本的KeyValue的byte數組由3部分組成。

  • 2個長度字段,每個字段4字節:即key的長度,value的長度
  • key數據
  • value數據

 其中,key包括了rowkey,family,qualifier,timestamp,type,這5個部分。

  • rowkey:2字節的rowkey length字段描述其長度,所以,最大的rowkey長度就是Short所能描述的最大正整數,即Short#MAX_VALUE,32KB。
  • famliy:1字節的family length字段描述其長度,所以,最大的family長度是Byte#MAX_VALUE,即127字節
  • qualifier:不獨立存儲其長度,通過KeyLength,rowkey長度,family長度,可以計算得到qualifier長度。
  • timestamp:定長8字節,單位毫秒,時間戳
  • type:1字節,描述這個KV的類型,如Put還是delete marker等

 末尾是value字段,其長度由開始的ValueLength字段來定義。最大是Integer#MAX_VALUE,即4GB。但實際上,超過1MB的KV通常就會導致嚴重的性能問題了,超過64MB的KV一般來說Protobuffer很可能都無法支持其進行序列化和反序列化。所以,單純從這個意義上看,HBase本身並不適合存儲大對象(還有其他很多因素導致HBase不適合管理大塊數據,這里不展開)。

 

由上面的KeyValue格式定義可見,這個格式還是很直觀、符合直覺的,易理解,代碼也容易懂,沒有玩什么奇技淫巧。

KeyValue格式(1.x/2.x版本)

與094版本相比,末尾多了一個tag區段,可以存儲任意數量的tag。tag提供了一個擴展KeyValue能力的途徑,比如行級/列級TTL,可以通過將TTL記錄在tag中來實現。用戶也可以存儲一些自定義的信息。

使用KeyValue

這里想討論一下如果通過KeyValue的接口來獲取KeyValue內部的各個字段的數據。前面已經說了,KeyValue本身就是一串連續的二進制數據,內部使用一個byte[]來組織。那么,從KeyValue中獲取任何一個字段的數據,本質上都是從這個byte[]中截取一段,然后返回。這必然涉及到兩種數據獲取方式:

  • 拷貝一次:如KeyValue#getRow(), getValue()等方法,創建一個新的byte[],將內部的byte[]中對應的字段的二進制拷貝到新的數組中,然后返回新的數組
  • 返回ptr:即返回一個3元組,(byte[], offset, length),讓用戶自己根據offset來在一個byte[]定位起始位置,讀取指定長度的數據,即可得到需要的字段的內容(參見Cell接口的定義)。KeyValue為每個字段提供了3個接口來實現這個功能,這里以Value為例:
    • getValueArray():返回一個byte[],這個數組中存儲了value
    • getValueOffset():返回一個int,指示value字段的起始的字節偏移量
    • getValueLength():返回一個int,指示value字段的實際長度,單位是字節

拷貝一次這種方式,使用簡便,代碼易維護,但性能較差。尤其是在高吞吐的系統中,多一次內存拷貝,會浪費大量的CPU。第二種方式使用起來稍顯麻煩,但開銷較小,比較常用。應根據實際需要選擇最合適的接口。

 

其他

KeyValue的格式是了解HBase底層存儲結構的第一步,還有其他一些關鍵的設計,包括:

各類KeyValue comparator的實現,及他們的應用

DIFF壓縮的原理,及其對讀寫鏈路的影響

時間戳與多版本的原理和應用

KeyValue在HFile和HLog中的存儲,即HLog的格式和HFile的格式

讀鏈路:KeyValue的查找

meta表(root表)的設計:HBase表設計的典范

rowkey相關的一些列問題:rowkey的設計,前綴掃描,復合主鍵實現與數據類型編碼,等等

。。。

后續會慢慢對這些問題和設計進行整理和分析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM