原創文章,轉載請注明原作地址:http://www.lxweimin.com/p/0f9578df7fbc
一. 架構
1. 數據模型
1.1 基礎概念
- 表(table):列式存儲,支持高表&寬表(上億行,上百萬列)
- 行(row):每一行由唯一的行鍵確定
- 列族(columnFamily):每一行包含一個或多個列族,是列的集合
- 列(column):列式存儲,列是最基本單位,可能有多個版本的值
- 時間戳(Timestamp):列的不同版本之間用時間戳區分
- 單元格(cell):列的每一個版本是一個單元格,是存儲的基本單位
HBase最基本的單位是列(column),一列或者多列形成一行(row),若干行數據組成了一張表(table)。聽起來是一個非常普通的列式存儲數據庫,但是,它和傳統數據庫有很大的不同。
1.2 與傳統數據庫的區別
a. HBase的每一行由唯一的行鍵確定
在某種程度上,行鍵相當于傳統數據庫的primary key,區別在于,primary key是可選的,而HBase的每張表都必然會有行鍵。除了行鍵之外,HBase表不能對列添加索引。HBase是一個<key, value>形式的數據庫,行鍵就是它的key。
b. HBase引入了列族(columnFamily)的概念
- HBase是一個列式存儲的數據庫,因此列的使用是非常靈活的,不必在表定義的時候就定好列名,但是必須在建表的時候定義好列族名字。
- 一張HBase表存儲的列的數量可以是無限的,但是列族的數量最好控制在2-3個(原因在備注中[1])
- 列必須屬于某個列族,不同列族之間可以有同名列
- 列族的作用是,將那些數據量和屬性相似的列聚集在一起,以便我們給這些列定義一些共同的存儲方式屬性(e.g. 數據壓縮,保存到讀緩存中)
c. HBase的列值可以有多版本
在HBase表中,行鍵、列族、列名和時間戳才能唯一確定一個值。每個值是一個單元格(cell),是存儲的基本單位。每一行數據的每一列,都可以存儲多個值,每個版本的值之間通過時間戳確定,在存儲的時候,這些值也按照時間戳逆序排列,保證客戶端永遠讀到最新的數據。但是,每個列族可以存儲的最大版本數是確定的,并且是在建表的時候就定義好的。
另外,單元格的時間戳是可以由用戶自行指定的,如果不指定,服務器就會將接收到寫請求的服務器時間作為單元格的時間戳。通常情況下,最好不要自己指定時間戳,因為客戶端總是難以保證,指定的時間戳是按照寫順序遞增的。
d. 反范式化
HBase是一個NoSQL(Not-only-SQL)數據庫,不提供復雜的查詢方式,包括join。另外,相對于MySQL,HBase的可擴展性很好,存儲資源要廉價很多。因此,在設計數據庫的時候,我們總是傾向于反范式化,以方便后期的數據查詢
1.3 數據模型抽象
HBase實際上是按照谷歌的bigtable實現的,而谷歌在bigtable論文的開篇就介紹了bigtable的特點:A Bigtable is a sparse, distributed, persistent multidimensional sorted map。所以HBase在本質上,是一張有序的多維map,數據模型可以抽象成:
<rowKey : columnFamily : qualifier : timestamp, value>
這樣的優點是,HBase只存儲有值的單元格,對于一張稀疏表來說,可以節省很多存儲空間;但是,為每個cell都存儲了rowKey, columnFamily, qualifier,因此cf的名字不要太長。
2. 存儲模型
2.1 存儲概覽
a. 數據分片存儲
在HBase中,一張表的數據會被分成幾份,每一份數據為一個region;每個region內存儲的key是連續范圍內的,不同region存儲的key范圍不重合;這些region可能被存儲在同一臺機器上,也可能存儲在不同的機器上。HBase作為一個分布式數據庫,對數據進行分片,可以提升吞吐量。
b. HLog:Write-Ahead-Log,寫操作先寫日志
HLog的作用是,當一臺regionServer crash了,可以利用HLog來恢復內存中未持久化到硬盤中的數據。需要注意的是,同一臺server上的所有region共用一個HLog實例,因為假如每個region擁有一個獨立的HLog,服務器會花費很多時間在磁盤尋道上。
c. MemStore:寫緩存,每個store擁有獨立的寫緩存
在HBase中,所有的寫操作全部寫到內存中,當寫緩存(MemStore)寫滿,再刷寫(flush)到磁盤中[2],形成一個新的文件。這樣做的目的,是為了高速響應那些寫請求。
d. HFile:磁盤文件
在存儲上,HBase完全依賴HDFS,磁盤操作是直接調用HDFS的API(HDFS在維持data locality這一點上足夠智能)。另外,之前提過HBase定義列族的一個原因是為了方便存儲,事實上,同一列族的數據會被寫到同一文件,因為存儲特性本來就是按照列族定義的。HBase的數據在底層文件中時以KeyValue鍵值對的形式存儲的,HBase沒有數據類型,HFile中存儲的是字節,這些字節按字典序排列。
e. 讀緩存:同一server上所有region共用
既然HBase有寫緩存,相對應的應該有讀緩存。與寫緩存不同的是,HBase的讀緩存是同一server上的所有region共用的。當HBase讀取磁盤上某一條數據時,HBase會將整個HFile block[3]讀到cache中[4]。因此,當client請求臨近的數據時,因為臨近數據已經被緩存到內存中,HBase的響應會更快,也就是說,HBase鼓勵將那些相似的,會被一起查找的數據存放在一起。另外,當我們在做全表掃描時,為了不刷走讀緩存中的熱數據,千萬記得關閉讀緩存的功能。
2.2 行鍵的索引
a. 行數據查找步驟
- hbase:meta表查找,獲取數據所在的region id
- 根據region id,到對應的region server上查找,在server上查找對應記錄時,有三種方式
(1) 掃描緩存
(2) 塊索引
(3) 布隆過濾器
b. rowKey索引:hbase:meta表
client會首先獲取hbase:meta表的位置,再到對應的region server上讀取這張表的內容(hbase:meta表其實就是一張HBase表)。讀到這張hbase:meta表之后,client會緩存這張表,這張之后的查找就可以復用了。hbase:meta表的內容如下:
| key | value |
| ----------------------------------------------------------------- | ------------------------------------------------------------------------------------|
| | info:regioninfo (serialized HRegionInfo instance for this region) |
| Region key of the format ([table],[region start key],[region id]) | info:server (server:port of the RegionServer containing this region) |
| | info:serverstartcode (start-time of the RegionServer process containing this region)|
因此,通過查找hbase:meta表,client可以得知對應的數據存儲在哪臺server的那個region上,接下來就要到對應的server上查找相關數據了。
c. region server上的數據查找
當接收到一個讀請求,server會初始化一個scanner查找內存中是否有相關數據;一個scanner查找硬盤文件中是否存儲了相關數據。查找硬盤文件是一件相當繁重的體力活,為了加快文件查找,HBase借助了兩個工具:塊索引和布隆過濾器。
d. rowKey索引:塊索引
塊索引存儲在HFile文件中的末端,當HBase在查找文件中是否保存了目標數據時,首先會將塊索引讀入內存。因為HFile中的KeyValue字節數據,是按照字典序排列的,而塊索引存儲了文件中所有塊(HFile block)的起始key,因此可以根據塊索引迅速定位需要查找的塊,只將可能保存了目標數據的塊讀到內存中,能加快查找速度。
e. rowKey索引:布隆過濾器
雖然塊索引幫助減少了需要讀到內存中的數據,我們依然需要查找每個文件中的一個塊,才能完成磁盤數據查找,而布隆過濾器則可以幫助我們跳過那些顯然不包含目標數據的文件。因為布隆過濾器的特點是,能迅速判斷一個數據集合中包不包含目標數據,判斷結果有兩種,不包含和可能包含。如下圖所示,布隆過濾器能幫助跳過一些肯定不包含目標數據的文件。
和塊索引一樣,布隆過濾器也被存儲在文件末端,會被優先加載到內存中。另外,布隆過濾器分行式和列式兩種,列式需要更多的存儲空間,因此如果是按行讀取數據,沒必要使用列式的布隆過濾器。布隆過濾器和塊索引的對比如下:
塊索引 | 布隆過濾器 |
---|---|
快速定位記錄在文件中可能的位置 | 快速判斷文件中是否包含相應記錄 |
2.3 將隨機寫轉化成順序寫
HBase的存儲是完全基于HDFS的,而HDFS的特點是不能對磁盤文件進行隨機修改。因此,HBase無法對已寫入磁盤文件的表記錄進行隨機修改,但是對于數據庫來說,支持對表記錄進行隨機修改是基本功能。為此,HBase的方法是將隨機寫的操作轉化成順序寫。
首先,隨機的寫操作轉化為文件追加操作,按照時間順序排列,client讀數據時總是優先讀到最新的修改。而刪除操作則轉化為寫入一個tombstone標記,標記著早于這個tombstone時間戳的對應行所有記錄作廢。
顯然,因為HBase總是進行文件追加,隨著時間積累,文件膨脹很快。major compact的一個作用就是,真正刪除所有無效的過時數據。
2.4 HBase Compaction和Region Split
a. HBase Compaction
前面已經提到過,HBase數據寫入的時候,總是先寫入到寫緩存(MemStore)中,當寫緩存寫滿,則flush到磁盤形成一個新的磁盤文件。可以想象的是,隨著時間增長,磁盤上這樣的小文件會越來越多,HBase查找數據也需要越來越長的時間。為了避免這樣的問題,HBase會做compaction,合并HFile文件,減少每次查找數據的磁盤尋道時間。compaction分為major compact和minor compact兩種:
- Minor compact:將多個小文件簡單合并成一個大文件
- Major compact:將同一列族的所有文件合并成一個大文件,并且刪除過期無效的數據和tombstone標記[5]
b. Region Split
client不斷向HBase寫入數據,region管理的數據量不斷膨脹。當一個region內存儲的數據量到達閾值,則會觸發HBase的region split操作,將老的region拆分成兩個新的子region。拆分的原則是數據量對半分。為了避免region拆分導致的IO瞬時上升,region拆分并不會立刻將拆分重寫所有的磁盤文件文件,而是為每個子region創建reference文件,這些文件指向了舊的磁盤文件中對應記錄的起始和終止位置。等到子region的compact操作被觸發,在重寫文件的時候,HBase才會為每個子region生成獨立的磁盤文件。
3. 物理模型
HBase的架構是一個典型的master-slave模型,HBase的master節點叫HMaster,slave節點就是RegionServer。
3.1 Master的職責
處理集群相關的請求(來自client或者其他server節點)
- 建表或者表變更的操作
- 打開或者關閉一個region
- metadata元數據的管理
集群監控(依賴Zookeeper)
- 監控regionServer的狀態以及負載均衡等
- 跟蹤hbase:meta表的位置
- 后備master節點需要監控當前master節點是否活躍
3.2 RegionServer的職責
集群初始化過程中
當一個HBase集群起來之后,HMaster會在對應的regionServer上起一個HRegionServer進程。HRegionServer負責打開對應的region,并創建對應的HRegion實例。當HRegion打開之后,它會為每個表的HColumnFamily創建一個Store實例,ColumnFamily是用戶在創建表時定義好的,ColumnFamily在每個region中和Store實例一一對應。每個Store實例包含一個或者多個StoreFile實例,StoreFile是對實際存儲數據文件HFile的輕量級封裝。每個Store對應一個MemStore。一個HRegionServer共享一個HLog實例。
集群運行過程中
- compaction和split是由RegionServer獨立判斷決定是否執行的,但過程中包含一些必要的和master、ZK的通信
- client端發起的讀寫請求,也直接由對應RegionServer處理(master不處理)[6],流程如下:
(1) client端向ZK請求hbase:meta表位置,取得表內容
(2) 查詢meta表得知數據存在哪臺RegionServer上
(3)直接與RegionServer通信,進行讀寫操作
注:
[1] 同一張表的column family數量不能超過2-3個。因為目前,flush和compaction操作是基于region進行的,當一個column family觸發了MemStore flush操作,相鄰的column family都會被刷寫到磁盤,即使它們MemStore內的數據量還很小。因此,如果同一張表內column family的數量過多,flush和compaction將會帶來更多不必要的I/O負載(當然這個問題可以通過,將flush和compaction改成列族之間互不影響來解決)。通常情況下,定義表的時候盡量使用單列族,除非列與列的查詢是相對獨立的,再考慮使用多個列族,比如client并不會同時請求兩個列族的數據。
當同一張內有多個列族時,注意一些列族間的數據量是否一致,假如列族A和列族B的數據量相差懸殊,列族A的大數據量會導致表數據被分片到很多個機器上,此時再對列族B的數據做掃描,效率會很低。
[2] 能觸發MemStore flush操作的有三種情形:
- 當一個MemStore的數據量達到hbase.hregion.memstore.flush.size,同一region的所有MemStore的數據都會被刷寫道磁盤
- 當全部的MemStore的數據量達到hbase.regionserver.global.memstore.upperLimit,同一RegionServer的多個region的MemStore的數據會被刷寫到磁盤。按照每region的MemStore大小,從大到小刷寫到磁盤,直到總的MemStore大小下降到hbase.regionserver.global.memstore.upperLimit
- 當region server的WAL的log數量達到hbase.regionserver.max.logs,該server上多個region的MemStore會被刷寫到磁盤(按照時間順序),以降低WAL的大小。
[3] HFile block:HFile塊和Hadoop塊是兩個獨立的概念。HFile塊的默認大小是64KB,而Hadoop塊的默認大小為64MB。另外,如果有需要的話,用戶還可以自行定義HFile塊大小。一般情況下,如果客戶端都是順序訪問表記錄,在讀緩存的作用下,建議使用較大的HFile塊;如果客戶端都是隨機訪問表記錄,建議使用較小的HFile塊,不過也需要更多的內存來存儲塊索引(塊索引會優先存放在cache中),并且創建過程也會變得更慢,因為我們必須在每個數據塊結束的時候刷寫壓縮流,導致一個FS I/O刷寫。
[4] 關于讀緩存的更詳細資料:http://zh.hortonworks.com/blog/hbase-blockcache-101/
[5] HBase默認每7天對HBase做一輪major compact,在0.96的版本之前,這個周期是1天。
[6] 正因為client讀寫數據的過程沒有master節點的參與,如果master failover了,hbase集群仍然可以穩定運行一段時間,只是像region分裂,RegionServer failover處理等需要master節點參與的工作,無法完成了。
未完待續...