Hbase的Rowkey設計原則
一、 Hbase介紹
HBase -> Hadoop Database,HBase是Apache的Hadoop項目的子項目。HBase不同於一般的關系數據庫,它是一個適合於非結構化數據存儲的數據庫。另一個不同的是HBase基於列的而不是基於行的模式,主要用來存儲非結構化和半結構化的松散數據(列存NoSQL數據庫)
二、 設計原則
2.1 Rowkey長度原則
Rowkey是一個二進制碼流,Rowkey的長度被很多開發者建議設計在10-100個字節,不過建議是越短越好,不要超過16個字節。
原因如下:
(1)數據的持久化文件HFile中是按照KeyValue存儲的,如果Rowkey過長比如100個字節,1000萬列數據光Rowkey就要占用100*1000萬=10億個字節,將近1G數據,這會極大影響Hfile的存儲效率;
(2)MemStore將緩存部分數據到內存,如果Rowkey字段過長內存的有效利用率降低,系統將無法緩存更多的數據,這會降低檢索效率,因此Rowkey的字節長度越短越好。
(3)目前操作系統一般都是64位系統,內存8字節對齊,空值在16個字節,8字節的整數倍利用操作系統的最佳特性。
2.2 Rowkey散列原則
如果Rowkey是按時間戳的方式遞增,不要將時間放在二進制碼的前面,建議將Rowkey的高位作為散列字段,由程序循環生成,低位放時間字段,這樣將提高數據均衡分布在每個Regionserver實現負載均衡的幾率。如果沒有散列字段,首字段直接是時間信息將產生所有新數據都在一個 RegionServer上堆積的熱點現象,這樣在做數據檢索的時候負載將會集中在個別RegionServer,降低查詢效率。
2.3 Rowkey唯一原則
必須在設計Rowkey上保證其唯一性。
2.4 訪問hbase table中的行,只有三種方式:
1 通過單個row key訪問
2 通過row key的range
3 全表掃描
Hbase API文檔:http://hbase.apache.org/apidocs/index.html?overview-summary.html
HBase的查詢實現只提供兩種方式:
1、按指定RowKey獲取唯一一條記錄,get方法(org.apache.hadoop.hbase.client.Get)
2、按指定的條件獲取一批記錄,scan方法(org.apache.hadoop.hbase.client.Scan)
實現條件查詢功能使用的就是scan方式,scan在使用時有以下幾點值得注意:
1、 scan可以通過setCaching與setBatch方法提高速度(以空間換時間);
2、 scan可以通過setStartRow與setEndRow來限定范圍。范圍越小,性能越高。
通過巧妙的RowKey設計使我們批量獲取記錄集合中的元素挨在一起(應該在同一個 Region下),可以在遍歷結果時獲得很好的性能。
3、 scan可以通過setFilter方法添加過濾器,這也是分頁、多條件查詢的基礎。
三、 應用場景
3.1 針對事務數據Rowkey設計
事務數據是帶時間屬性的,建議將時間信息存入到Rowkey中,這有助於提示查詢檢索速度。對於事務數據建議缺省就按天為數據建表,這樣設計的好處是多方面的。按天分表后,時間信息就可以去掉日期部分只保留小時分鍾毫秒,這樣4個字節即可搞定。加上散列字段2個字節一共6個字節即可組成唯一 Rowkey。如下圖所示:
事務數據Rowkey設計 |
||||||
第0字節 |
第1字節 |
第2字節 |
第3字節 |
第4字節 |
第5字節 |
… |
散列字段 |
時間字段(毫秒) |
擴展字段 |
||||
0~65535(0x0000~0xFFFF) |
0~86399999(0x00000000~0x05265BFF) |
|
這樣的設計從操作系統內存管理層面無法節省開銷,因為64位操作系統是必須8字節對齊。但是對於持久化存儲中Rowkey部分可以節省25%的開銷。也許有人要問為什么不將時間字段以主機字節序保存,這樣它也可以作為散列字段了。這是因為時間范圍內的數據還是盡量保證連續,相同時間范圍內的數據查找的概率很大,對查詢檢索有好的效果,因此使用獨立的散列字段效果更好,對於某些應用,我們可以考慮利用散列字段全部或者部分來存儲某些數據的字段信息,只要保證相同散列值在同一時間(毫秒)唯一。
針對統計數據的Rowkey設計
統計數據也是帶時間屬性的,統計數據最小單位只會到分鍾(到秒預統計就沒意義了)。同時對於統計數據我們也缺省采用按天數據分表,這樣設計的好處無需多說。按天分表后,時間信息只需要保留小時分鍾,那么0~1400只需占用兩個字節即可保存時間信息。由於統計數據某些維度數量非常龐大,因此需要4個字節作為序列字段,因此將散列字段同時作為序列字段使用也是6個字節組成唯一Rowkey。如下圖所示:
統計數據Rowkey設計 |
||||||
第0字節 |
第1字節 |
第2字節 |
第3字節 |
第4字節 |
第5字節 |
… |
散列字段(序列字段) |
時間字段(分鍾) |
擴展字段 |
||||
0x00000000~0xFFFFFFFF) |
0~1439(0x0000~0x059F) |
|
同樣這樣的設計從操作系統內存管理層面無法節省開銷,因為64位操作系統是必須8字節對齊。但是對於持久化存儲中Rowkey部分可以節省25%的開銷。預統計數據可能涉及到多次反復的重計算要求,需確保作廢的數據能有效刪除,同時不能影響散列的均衡效果,因此要特殊處理。
針對通用數據的Rowkey設計
通用數據采用自增序列作為唯一主鍵,用戶可以選擇按天建分表也可以選擇單表模式。這種模式需要確保同時多個入庫加載模塊運行時散列字段(序列字段)的唯一性。可以考慮給不同的加載模塊賦予唯一因子區別。設計結構如下圖所示。
通用數據Rowkey設計 |
||||
第0字節 |
第1字節 |
第2字節 |
第3字節 |
… |
散列字段(序列字段) |
擴展字段(控制在12字節內) |
|||
0x00000000~0xFFFFFFFF) |
可由多個用戶字段組成 |
支持多條件查詢的RowKey設計
下面舉個形象的例子:
我們在表中存儲的是文件信息,每個文件有5個屬性:文件id(long,全局唯一)、創建時間(long)、文件名(String)、分類名(String)、所有者(User)。
我們可以輸入的查詢條件:文件創建時間區間(比如從20120901到20120914期間創建的文件),文件名(“快樂大本營”),分類(“綜藝”),所有者(“浙江衛視”)。
假設當前我們一共有如下文件:
內容列表 ID CreateTime Name Category UserID 1 2 3 4 5 6 7 8 9 10
20120902 |
快樂大本營第1期 |
綜藝 |
1 |
20120904 |
快樂大本營第2期 |
綜藝 |
1 |
20120906 |
快樂大本營番外 |
綜藝 |
1 |
20120908 |
快樂大本營第3期 |
綜藝 |
1 |
20120910 |
快樂大本營第4期 |
綜藝 |
1 |
20120912 |
快樂大本營嘉賓采訪 |
綜藝花絮 |
2 |
20120914 |
快樂大本營第5期 |
綜藝 |
1 |
20120916 |
快樂大本營錄制花絮 |
綜藝花絮 |
2 |
20120918 |
王祖藍獨家專訪 |
花絮 |
3 |
20120920 |
安慕希酸奶廣告 |
綜藝廣告 |
4 |
這里UserID應該對應另一張User表,暫不列出。我們只需知道UserID的含義:
1代表 浙江衛視; 2代表 大本營劇組; 3代表 XX微博; 4代表 贊助商。
調用查詢接口的時候將上述5個條件同時輸入find(20120901,20121001,"快樂大本營","綜藝","浙江衛視")。
此時我們應該得到記錄應該有第1、2、3、4、5、7條。第6條由於不屬於“浙江衛視”應該不被選中。
我們在設計RowKey時可以這樣做:采用UserID + CreateTime + FileID組成rowKey,這樣既能滿足多條件查詢,又能有很快的查詢速度。
需要注意以下幾點:
1、每條記錄的RowKey,每個字段都需要填充到相同長度。假如預期我們最多有10萬量級的用戶,則userID應該統一填充至6位,如000001,000002...
2、結尾添加全局唯一的FileID的用意也是使每個文件對應的記錄全局唯一。避免當UserID與CreateTime相同時的兩個不同文件記錄相互覆蓋。
按照這種RowKey存儲上述文件記錄,在HBase表中是下面的結構:
rowKey(userID 6 + time 8 + fileID 6) name category ....
00000120120902000001
00000120120904000002
00000120120906000003
00000120120908000004
00000120120910000005
00000120120914000007
00000220120912000006
00000220120916000008
00000320120918000009
00000420120920000010
.....
怎樣用這張表?
在建立一個scan對象后,我們setStartRow(00000120120901),setEndRow(00000120120914)。
這樣,scan時只掃描userID=1的數據,且時間范圍限定在這個指定的時間段內,滿足了按用戶以及按時間范圍對結果的篩選。並且由於記錄集中存儲,性能很好。
然后使用SingleColumnValueFilter(org.apache.hadoop.hbase.filter.SingleColumnValueFilter),共4個,分別約束name的上下限,與category的上下限。滿足按同時按文件名以及分類名的前綴匹配。
(注意:使用SingleColumnValueFilter會影響查詢性能,在真正處理海量數據時會消耗很大的資源,且需要較長的時間。)
如果需要分頁還可以再加一個PageFilter限制返回記錄的個數。
以上,我們完成了高性能的支持多條件查詢的HBase表結構設計。
四、 什么是熱點
HBase中的行是按照rowkey的字典順序排序的,這種設計優化了scan操作,可以將相關的行以及會被一起讀取的行存取在臨近位置,便於scan。然而糟糕的rowkey設計是熱點的源頭。熱點發生在大量的client直接訪問集群的一個或極少數個節點(訪問可能是讀,寫或者其他操作)。大量訪問會使熱點region所在的單個機器超出自身承受能力,引起性能下降甚至region不可用,這也會影響同一個RegionServer上的其他region,由於主機無法服務其他region的請求。設計良好的數據訪問模式以使集群被充分,均衡的利用。
為了避免寫熱點,設計rowkey使得不同行在同一個region,但是在更多數據情況下,數據應該被寫入集群的多個region,而不是一個。
下面是一些常見的避免熱點的方法以及它們的優缺點:
1. 加鹽
這里所說的加鹽不是密碼學中的加鹽,而是在rowkey的前面增加隨機數,具體就是給rowkey分配一個隨機前綴以使得它和之前的rowkey的開頭不同。分配的前綴種類數量應該和你想使用數據分散到不同的region的數量一致。加鹽之后的rowkey就會根據隨機生成的前綴分散到各個region上,以避免熱點。
2. 哈希
哈希會使同一行永遠用一個前綴加鹽。哈希也可以使負載分散到整個集群,但是讀卻是可以預測的。使用確定的哈希可以讓客戶端重構完整的rowkey,可以使用get操作准確獲取某一個行數據
3. 反轉
第三種防止熱點的方法時反轉固定長度或者數字格式的rowkey。這樣可以使得rowkey中經常改變的部分(最沒有意義的部分)放在前面。這樣可以有效的隨機rowkey,但是犧牲了rowkey的有序性。
3.1 反轉rowkey的例子
以手機號為rowkey,可以將手機號反轉后的字符串作為rowkey,這樣的就避免了以手機號那樣比較固定開頭導致熱點問題
3.2 時間戳反轉
一個常見的數據處理問題是快速獲取數據的最近版本,使用反轉的時間戳作為rowkey的一部分對這個問題十分有用,可以用Long.Max_Value - timestamp追加到key的末尾,例如[key][reverse_timestamp],[key]的最新值可以通過scan [key]獲得[key]的第一條記錄,因為HBase中rowkey是有序的,第一條記錄是最后錄入的數據。
比如需要保存一個用戶的操作記錄,按照操作時間倒序排序,在設計rowkey的時候,可以這樣設計
[userId反轉][Long.Max_Value - timestamp],在查詢用戶的所有操作記錄數據的時候,直接指定反轉后的userId,startRow是[userId反轉][000000000000],stopRow是[userId反轉][Long.Max_Value - timestamp]
如果需要查詢某段時間的操作記錄,startRow是[user反轉][Long.Max_Value - 起始時間],stopRow是[userId反轉][Long.Max_Value - 結束時間]
3.3 盡量減少行和列的大小
在HBase中,value永遠和它的key一起傳輸的。當具體的值在系統間傳輸時,它的rowkey,列名,時間戳也會一起傳輸。如果你的rowkey和列名很大,HBase storefiles中的索引(有助於隨機訪問)會占據HBase分配的大量內存,因為具體的值和它的key很大。可以增加block大小使得storefiles索引再更大的時間間隔增加,或者修改表的模式以減小rowkey和列名的大小。壓縮也有助於更大的索引。