HBase Rowkey的雜湊與預分割槽設計

問題導讀:

1。如何防止熱點?

2。如何預分割槽?

擴充套件:

為什麼會產生熱點儲存?

HBase中,表會被劃分為1。。。n個Region,被託管在RegionServer中。Region二個重要的屬性:StartKey與EndKey表示這個Region維護的rowKey範圍,當我們要讀/寫資料時,如果rowKey落在某個start-end key範圍內,那麼就會定位到目標region並且讀/寫到相關的資料。簡單地說,有那麼一點點類似人群劃分,1-15歲為小朋友,16-39歲為年輕人,40-64為中年人,65歲以上為老年人。(這些數值都是拍腦袋出來的,只是舉例,非真實),然後某人找隊伍,然後根據年齡,處於哪個範圍,就找到它所屬的隊伍。 : ( 有點廢話了。。。。

然後,預設地,當我們只是透過HBaseAdmin指定TableDescriptor來建立一張表時,只有一個region,正處於混沌時期,start-end key無邊界,可謂海納百川。啥樣的rowKey都可以接受,都往這個region裡裝,然而,當資料越來越多,region的size越來越大時,大到一定的閥值,hbase認為再往這個region裡塞資料已經不合適了,就會找到一個midKey將region一分為二,成為2個region,這個過程稱為分裂(region-split)。而midKey則為這二個region的臨界,左為N無下界,右為M無上界。< midKey則為陰被塞到N區,> midKey則會被塞到M區。

如何找到midKey?涉及的內容比較多,暫且不去討論,最簡單的可以認為是region的總行數 / 2 的那一行資料的rowKey。雖然實際上比它會稍複雜點。

如果我們就這樣預設地,建表,表裡不斷地Put資料,更嚴重的是我們的rowkey還是順序增大的,是比較可怕的。存在的缺點比較明顯。

首先是熱點寫,我們總是會往最大的start-key所在的region寫東西,因為我們的rowkey總是會比之前的大,並且hbase的是按升序方式排序的。所以寫操作總是被定位到無上界的那個region中。

其次,由於寫熱點,我們總是往最大start-key的region寫記錄,之前分裂出來的region不會再被寫資料,有點被打進冷宮的趕腳,它們都處於半滿狀態,這樣的分佈也是不利的。

如果在寫比較頻率的場景下,資料增長快,split的次數也會增多,由於split是比較耗時耗資源的,所以我們並不希望這種事情經常發生。

…………

看到這些缺點,我們知道,在叢集的環境中,為了得到更好的並行性,我們希望有好的load blance,讓每個節點提供的請求處理都是均等的。我們也希望,region不要經常split,因為split會使server有一段時間的停頓,如何能做到呢?

隨機雜湊與預分割槽。二者結合起來,是比較完美的,預分割槽一開始就預建好了一部分region,這些region都維護著自已的start-end keys,再配合上隨機雜湊,寫資料能均等地命中這些預建的region,就能解決上面的那些缺點,大大地提高了效能。

提供2種思路: hash 與 partition。

hash就是rowkey前面由一串隨機字串組成,隨機字串生成方式可以由SHA或者MD5等方式生成,只要region所管理的start-end keys範圍比較隨機,那麼就可以解決寫熱點問題。

long currentId = 1L;byte [] rowkey = Bytes。add(MD5Hash。getMD5AsHex(Bytes。toBytes(currentId))。substring(0, 8)。getBytes(), Bytes。toBytes(currentId));

假設rowKey原本是自增長的long型,可以將rowkey轉為hash再轉為bytes,加上本身id 轉為bytes,組成rowkey,這樣就生成隨便的rowkey。那麼對於這種方式的rowkey設計,如何去進行預分割槽呢?

1。取樣,先隨機生成一定數量的rowkey,將取樣資料按升序排序放到一個集合裡

2。根據預分割槽的region個數,對整個集合平均分割,即是相關的splitKeys。

3。HBaseAdmin。createTable(HTableDescriptor tableDescriptor,byte[][] splitkeys)可以指定預分割槽的splitKey,即是指定region間的rowkey臨界值。

1。建立split計算器,用於從抽樣資料中生成一個比較合適的splitKeys

public class HashChoreWoker implements SplitKeysCalculator{ //隨機取機數目 private int baseRecord; //rowkey生成器 private RowKeyGenerator rkGen; //取樣時,由取樣數目及region數相除所得的數量。 private int splitKeysBase; //splitkeys個數 private int splitKeysNumber; //由抽樣計算出來的splitkeys結果 private byte[][] splitKeys; public HashChoreWoker(int baseRecord, int prepareRegions) { this。baseRecord = baseRecord; //例項化rowkey生成器 rkGen = new HashRowKeyGenerator(); splitKeysNumber = prepareRegions - 1; splitKeysBase = baseRecord / prepareRegions; } public byte[][] calcSplitKeys() { splitKeys = new byte[splitKeysNumber][]; //使用treeset儲存抽樣資料,已排序過 TreeSet rows = new TreeSet(Bytes。BYTES_COMPARATOR); for (int i = 0; i < baseRecord; i++) { rows。add(rkGen。nextId()); } int pointer = 0; Iterator rowKeyIter = rows。iterator(); int index = 0; while (rowKeyIter。hasNext()) { byte[] tempRow = rowKeyIter。next(); rowKeyIter。remove(); if ((pointer != 0) && (pointer % splitKeysBase == 0)) { if (index < splitKeysNumber) { splitKeys[index] = tempRow; index ++; } } pointer ++; } rows。clear(); rows = null; return splitKeys; }}

KeyGenerator及實現//interfacepublic interface RowKeyGenerator { byte [] nextId();}//implementspublic class HashRowKeyGenerator implements RowKeyGenerator { private long currentId = 1; private long currentTime = System。currentTimeMillis(); private Random random = new Random(); public byte[] nextId() { try { currentTime += random。nextInt(1000); byte[] lowT = Bytes。copy(Bytes。toBytes(currentTime), 4, 4); byte[] lowU = Bytes。copy(Bytes。toBytes(currentId), 4, 4); return Bytes。add(MD5Hash。getMD5AsHex(Bytes。add(lowU, lowT))。substring(0, 8)。getBytes(), Bytes。toBytes(currentId)); } finally { currentId++; } }}

unit test case測試

@Testpublic void testHashAndCreateTable() throws Exception{ HashChoreWoker worker = new HashChoreWoker(1000000,10); byte [][] splitKeys = worker。calcSplitKeys(); HBaseAdmin admin = new HBaseAdmin(HBaseConfiguration。create()); TableName tableName = TableName。valueOf(“hash_split_table”); if (admin。tableExists(tableName)) { try { admin。disableTable(tableName); } catch (Exception e) { } admin。deleteTable(tableName); } HTableDescriptor tableDesc = new HTableDescriptor(tableName); HColumnDescriptor columnDesc = new HColumnDescriptor(Bytes。toBytes(“info”)); columnDesc。setMaxVersions(1); tableDesc。addFamily(columnDesc); admin。createTable(tableDesc ,splitKeys); admin。close(); }

檢視建表結果:執行 scan ‘hbase:meta’

HBase Rowkey的雜湊與預分割槽設計

以上我們只是顯示了部分region的資訊,可以看到region的start-end key 還是比較隨機雜湊的。同樣可以檢視hdfs的目錄結構,的確和預期的38個預分割槽一致:

HBase Rowkey的雜湊與預分割槽設計

以上,就已經按hash方式,預建好了分割槽,以後在插入資料的時候,也要按照此rowkeyGenerator的方式生成rowkey,有興趣的話,也可以做些試驗,插入些資料,看看資料的分佈。

partition故名思義,就是分割槽式,這種分割槽有點類似於mapreduce中的partitioner,將區域用長整數(Long)作為分割槽號,每個region管理著相應的區域資料,在rowKey生成時,將id取模後,然後拼上id整體作為rowKey。這個比較簡單,不需要取樣,splitKeys也非常簡單,直接是分割槽號即可。直接上程式碼吧:

public class PartitionRowKeyManager implements RowKeyGenerator, SplitKeysCalculator { public static final int DEFAULT_PARTITION_AMOUNT = 20; private long currentId = 1; private int partition = DEFAULT_PARTITION_AMOUNT; public void setPartition(int partition) { this。partition = partition; } public byte[] nextId() { try { long partitionId = currentId % partition; return Bytes。add(Bytes。toBytes(partitionId), Bytes。toBytes(currentId)); } finally { currentId++; } } public byte[][] calcSplitKeys() { byte[][] splitKeys = new byte[partition - 1][]; for(int i = 1; i < partition ; i ++) { splitKeys[i-1] = Bytes。toBytes((long)i); } return splitKeys; }}

calcSplitKeys方法比較單純,splitKey就是partition的編號,我們看看測試類:

@Test public void testPartitionAndCreateTable() throws Exception{ PartitionRowKeyManager rkManager = new PartitionRowKeyManager(); //只預建10個分割槽 rkManager。setPartition(10); byte [][] splitKeys = rkManager。calcSplitKeys(); HBaseAdmin admin = new HBaseAdmin(HBaseConfiguration。create()); TableName tableName = TableName。valueOf(“partition_split_table”); if (admin。tableExists(tableName)) { try { admin。disableTable(tableName); } catch (Exception e) { } admin。deleteTable(tableName); } HTableDescriptor tableDesc = new HTableDescriptor(tableName); HColumnDescriptor columnDesc = new HColumnDescriptor(Bytes。toBytes(“info”)); columnDesc。setMaxVersions(1); tableDesc。addFamily(columnDesc); admin。createTable(tableDesc ,splitKeys); admin。close(); }

同樣我們可以看看meta表和hdfs的目錄結果,其實和hash類似,region都會分好區,在這裡就不上圖了。

透過partition實現的loadblance寫的話,當然生成rowkey方式也要結合當前的region數目取模而求得,大家同樣也可以做些實驗,看看資料插入後的分佈。

在這裡也順提一下,如果是順序的增長型原id,可以將id儲存到一個數據庫,傳統的也好,redis的也好,每次取的時候,將數值設大1000左右,以後id可以在記憶體內增長,當記憶體數量已經超過1000的話,再去load下一個,有點類似於oracle中的sqeuence。

隨機分佈加預分割槽也不是一勞永逸的。因為資料是不斷地增長的,隨著時間不斷地推移,已經分好的區域,或許已經裝不住更多的資料,當然就要進一步進行split了,同樣也會出現效能損耗問題,所以我們還是要規劃好資料增長速率,觀察好資料定期維護,按需分析是否要進一步分行手工將分割槽再分好,也或者是更嚴重的是新建表,做好更大的預分割槽然後進行資料遷移。小吳只是菜鳥,運維方面也只是自已這樣認為而已,供大家作簡單的參考吧。如果資料裝不住了,對於partition方式預分割槽的話,如果讓它自然分裂的話,情況分嚴重一點。因為分裂出來的分割槽號會是一樣的,所以計算到partitionId的話,其實還是回到了順序寫年代,會有部分熱點寫問題出現,如果使用partition方式生成主鍵的話,資料增長後就要不斷地調整分割槽了,比如增多預分割槽,或者加入子分割槽號的處理。(我們的分割槽號為long型,可以將它作為多級partition)

OK,寫到這裡,基本已經講完了防止熱點寫使用的方法和防止頻繁split而採取的預分割槽。但rowkey設計,遠遠也不止這些,比如rowkey長度,然後它的長度最大可以為char的MAXVALUE,但是看過之前我寫KeyValue的分析知道,我們的資料都是以KeyValue方式儲存在MemStore或者HFile中的,每個KeyValue都會儲存rowKey的資訊,如果rowkey太大的話,比如是128個位元組,一行10個欄位的表,100萬行記錄,光rowkey就佔了1。2G+所以長度還是不要過長,另外設計,還是按需求來吧。

最後題外話是我想分享我在github中建了一個project,希望做一些hbase一些工具:https://github。com/bdifn/hbase-tools,如果本地裝了git的話,可以執行命令: git clone https://github。com/bdifn/hbase-tools。git目前加了一個region-helper子專案,也是目前唯一的一個子專案,專案使用maven管理,主要目的是幫助我們設計rowkey做一些參考,比如我們設計的隨機寫和預分割槽測試,提供了抽樣的功能,提供了檢測隨機寫的功能,然後統計按目前rowkey設計,隨機寫n條記錄後,統計每個region的記錄數,然後顯示比例等。

測試模擬模組我程為simualtor,主要是模擬hbase的region行為,simple的實現,僅僅是上面提到的預測我們rowkey設計後,建好預分割槽後,寫資料的的分佈比例,而emulation是比較逼真的模擬,設想是我們寫資料時,會統計數目的大小,根據我們的hbase-site。xml設定,模擬memStore行為,模擬hfile的行為,最終會生成一份表的報表,比如分割槽的資料大小,是否split了,等等,以供我們去設計hbase表時有一個參考,但是遺憾的是,由於時間關係,我只花了一點業餘時間簡單搭了一下框架,目前沒有更一步的實現,以後有時間再加以完善,當然也歡迎大家一起加入,一起學習吧。

HBase Rowkey的雜湊與預分割槽設計

專案使用maven管理,為了方便測試,一些元件的例項化,我使用了java的SPI,download原始碼後,如果想測試自已的rowKeyGeneator的話,開啟com。bdifn。hbasetools。regionhelper。rowkey。RowKeyGenerator檔案後,替換到你們的ID生成器就可以了。如果是hash的話,抽樣和測試等,都是可以複用的。

如測試程式碼:

public class HBaseSimulatorTest { //透過SPI方式獲取HBaseSimulator例項,SPI的實現為simgple private HBaseSimulator hbase = BeanFactory。getInstance()。getBeanInstance(HBaseSimulator。class); //獲取RowKeyGenerator例項,SPI的實現為hashRowkey private RowKeyGenerator rkGen = BeanFactory。getInstance()。getBeanInstance(RowKeyGenerator。class); //初如化苦工,去檢測100w個抽樣rowkey,然後生成一組splitKeys HashChoreWoker worker = new HashChoreWoker(1000000,10); @Test public void testHash(){ byte [][] splitKeys = worker。calcSplitKeys(); hbase。createTable(“user”, splitKeys); //插入1億條記錄,看資料分佈 TableName tableName = TableName。valueOf(“user”); for(int i = 0; i < 100000000; i ++) { Put put = new Put(rkGen。nextId()); hbase。put(tableName, put); } hbase。report(tableName); } @Test public void testPartition(){ //default 20 partitions。 PartitionRowKeyManager rkManager = new PartitionRowKeyManager(); byte [][] splitKeys = rkManager。calcSplitKeys(); hbase。createTable(“person”, splitKeys); TableName tableName = TableName。valueOf(“person”); //插入1億條記錄,看資料分佈 for(int i = 0; i < 100000000; i ++) { Put put = new Put(rkManager。nextId()); hbase。put(tableName, put); } hbase。report(tableName); }}

執行結果:

Execution Reprort:[StartRowkey:puts requsts:(put ratio)]:9973569:(1。0015434)1986344a\x00\x00\x00\x00\x00\x01\x0E\xAE:9999295:(1。0041268)331ee65f\x00\x00\x00\x00\x00\x0F)g:10012532:(1。005456)4cbfd4f6\x00\x00\x00\x00\x00\x00o0:9975842:(1。0017716)664c6388\x00\x00\x00\x00\x00\x02\x1Du:10053337:(1。0095537)800945e0\x00\x00\x00\x00\x00\x01\xADV:9998719:(1。0040689)99a158d9\x00\x00\x00\x00\x00\x0BZ\xF3:10000563:(1。0042541)b33a2223\x00\x00\x00\x00\x00\x07\xC6\xE6:9964921:(1。000675)ccbcf370\x00\x00\x00\x00\x00\x00*\xE2:9958200:(1。0)e63b8334\x00\x00\x00\x00\x00\x03g\xC1:10063022:(1。0105262)total requests:100000000Execution Reprort:[StartRowkey:puts requsts:(put ratio)]:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x01:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x02:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x03:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x04:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x05:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x06:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x07:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x08:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x09:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0A:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0B:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0C:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0D:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0E:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x0F:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x10:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x11:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x12:5000000:(1。0)\x00\x00\x00\x00\x00\x00\x00\x13:5000000:(1。0)total requests:100000000

【本篇文章】以及Github會持續更新……另外作者提供多年悉心整理的計算機各類專業影片教學,如Kafka、Mysql、Tomcat、Docker、Spring、MyBatis、Nginx、Netty、Dubbo、Redis、Netty、Spring cloud、分散式、高併發、效能調優、微服務等,值得學習,可直接 下栽,請點選下面連結 [

瞭解更多

]。。。