轉自:http://www.aboutyun.com/thread-7119-1-1.html
對於任何系統的數據設計,我們都想提高性能,達到資源最大化利用,那么對於hbase我們產生如下問題:
1.hbase rowkey設計如何才能提高性能?
2.hbase rowkey如何設計才能散列到不同的節點上?
訪問hbase table中的行,只有三種方式:
1 通過單個row key訪問
2 通過row key的range
3 全表掃描
文中可能涉及到的API:
Hadoop/HDFS:http://hadoop.apache.org/common/docs/current/api/
HBase: 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方法添加過濾器,這也是分頁、多條件查詢的基礎。
-------------------------------------------------------------------------------------------------------------------------------------------------
下面舉個形象的例子:
我們在表中存儲的是文件信息,每個文件有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會影響查詢性能,在真正處理海量數據時會消耗很大的資源,且需要較長的時間。
在后續的博文中我將多舉幾種應用場景下rowKey的,可以滿足簡單條件下海量數據瞬時返回的查詢功能)
如果需要分頁還可以再加一個PageFilter限制返回記錄的個數。
以上,我們完成了高性能的支持多條件查詢的HBase表結構設計。
-------------------------------------------------------------------------------------------------------------------------------------------------
如何散列存儲
即時間上連續的數據。這些數據可能來自於某個傳感器網絡、證券交易或者一個監控系統。它們顯著的特點就是rowkey中含有事件發生時間。帶來的一個問題便是HBase對於row的不均衡分布,它們被存儲在一個唯一的rowkey區間中,被稱為region,區間的范圍被稱為Start Key和End Key。
對於單調遞增的時間類型數據,很容易被散列到同一個Region中,這樣它們會被存儲在同一個服務器上,從而所有的訪問和更新操作都會集中到這一台服務器上,從而在集群中形成一個hot spot,從而不能將集群的整體性能發揮出來。
要解決這個問題是非常容易的,只需要將所有的數據散列到全部的Region上即可。這是可以做到的,比如,在rowkey前面加上一個非線程序列,常常有如下選擇:
Hash散列
您可以使用一個Hash前綴來保證所有的行被分發到多個Region服務器上。例如:
byte prefix =
(byte) (Long.hashCode(timestamp) % <number of regionservers>);
byte[] rowkey =
Bytes.add(Bytes.toBytes(prefix), Bytes.toBytes(timestamp);
這個公式可以產生足夠的數字,將數據散列到所有的Region服務器上。當然,公式里假定了Region服務器的數目。如果您打算后期擴容您的集群,那么您可以把它先設置為集群的整數倍。生成的rowkey類似下面:
0myrowkey-1,
1myrowkey-2, 2myrowkey-3, 0myrowkey-4, 1myrowkey-5, \
2myrowkey-6, …
當他們將按如下順序被發送到各個Region服務器上去:
0myrowkey-1
0myrowkey-4
1myrowkey-2
1myrowkey-5
…
換句話說,對於0myrowkey-1和0myrowkey-4的更新操作會被發送到同一個region服務器上去(假定它們沒有被散列到兩個region上去),1myrowkey-2和1myrowkey-5會被發送到同一台服務器上。
這種方式的缺點是,rowkey的范圍必須通過代碼來控制,同時對數據的訪問,可能要訪問多台region服務器。當然,可以通過多個線程同時訪問,來實現並行化的數據讀取。這種類似於只有map的MapReduce任務,可以大大增加IO的性能。
案例:Mozilla
Socoroo
Mozilla公司搭建了一個名為Socorro的crash報告系統,用來跟蹤Firefox和Thunderbird的crash記錄,存儲所有的用戶提交的關於程序非正常中止的報告。這些報告被順序訪問,通過Mozilla的開發團隊進行分析,使得它們的應用軟件更加穩定。
這些代碼是開源的,包含着Python寫的客戶端代碼。它們使用Thrift直接與HBase集群進行交互。下面的給出了代碼中用於Hash時間的部分:
def
merge_scan_with_prefix(self,table,prefix,columns):
“”"
A generator based
iterator that yields totally ordered rows starting with a
given prefix. The
implementation opens up 16 scanners (one for each leading
hex character of
the salt) simultaneously and then yields the next row in
order from the
pool on each iteration.
“”"
iterators = []
next_items_queue =
[]
for salt in
’0123456789abcdef’:
salted_prefix =
“%s%s” % (salt,prefix)
scanner = self.client.scannerOpenWithPrefix(table,
salted_prefix, columns)
iterators.append(salted_scanner_iterable(self.logger,self.client,
self._make_row_nice,salted_prefix,scanner))
# The i below is
so we can advance whichever scanner delivers us the polled
# item.
for i,it in
enumerate(iterators):
try:
next = it.next
next_items_queue.append([next(),i,next])
except
StopIteration:
pass
heapq.heapify(next_items_queue)
while 1:
try:
while 1:
row_tuple,iter_index,next= s = next_items_queue[0]
#tuple[1]
is the actual nice row.
yield
row_tuple[1]
s[0]
= next()
heapq.heapreplace(next_items_queue,s)
except
StopIteration:
heapq.heappop(next_items_queue)
except
IndexError:
return
這些Python代碼打開了一定數目的scanner,加上Hash后的前綴。這個前綴是一個單字符的,共有16個不同的字母。heapq對象將scanner的結果進行全局排序。
字段位置交換
在前面提到了Key部分掃描,您可以移動timestamp字段,將它放在前一個字段的前面。這種方法通過rowkey的組合來將一個順序遞增的timestamp字段放在rowkey的第二個位置上。
如果你的rowkey不單單含有一個字段,您可以交換它們的位置。如果你現在的rowkey只有一個timestamp字段,您有必要再選出一個字段放在rowkey中。當然,這也帶來了一個缺點,即您常常只能通過rowkey的范圍查詢來訪問數據,比如timestamp的范圍。