Hbase的表會被劃分為1....n個Region,被托管在RegionServer中。Region二個重要的屬性:Startkey與EndKey表示這個Region維護的rowkey的范圍画机,當(dāng)我們要讀寫數(shù)據(jù)時焊刹,如果rowkey落在某個start-end key范圍內(nèi)系任,那么就會定位到目標(biāo)region并且讀寫到相關(guān)的數(shù)據(jù)。
? ? 默認情況下虐块,當(dāng)我們通過hbaseAdmin指定TableDescriptor來創(chuàng)建一張表時俩滥,只有一個region正處于混沌時期,start-end key無邊界贺奠,可謂海納百川霜旧。所有的rowkey都寫入到這個region里,然后數(shù)據(jù)越來越多儡率,region的size越來越大時挂据,大到一定的閥值,hbase就會將region一分為二儿普,成為2個region崎逃,這個過程稱為分裂(region-split)。
? ? 如果我們就這樣默認建表眉孩,表里不斷的put數(shù)據(jù)个绍,更嚴重的是我們的rowkey還是順序增大的,是比較可怕的浪汪。存在的缺點比較明顯:首先是熱點寫巴柿,我們總是向最大的start key所在的region寫數(shù)據(jù),因為我們的rowkey總是會比之前的大死遭,并且hbase的是按升序方式排序的广恢。所以寫操作總是被定位到無上界的那個region中;其次呀潭,由于熱點钉迷,我們總是往最大的start key的region寫記錄,之前分裂出來的region不會被寫數(shù)據(jù)蜗侈,有點打入冷宮的感覺篷牌,他們都處于半滿狀態(tài),這樣的分布也是不利的踏幻。
? ? 如果在寫比較頻繁的場景下,數(shù)據(jù)增長太快戳杀,split的次數(shù)也會增多该面,由于split是比較耗費資源的夭苗,所以我們并不希望這種事情經(jīng)常發(fā)生。
? ? 在集群中為了得到更好的并行性隔缀,我們希望有好的load blance题造,讓每個節(jié)點提供的請求都是均衡的,我們也不希望猾瘸,region不要經(jīng)常split界赔,因為split會使server有一段時間的停頓,如何能做到呢牵触?
隨機散列與預(yù)分區(qū)二者結(jié)合起來淮悼,是比較完美的。預(yù)分區(qū)一開始就預(yù)建好了一部分region揽思,這些region都維護著自己的start-end keys袜腥,在配合上隨機散列,寫數(shù)據(jù)能均衡的命中這些預(yù)建的region钉汗,就能解決上面的那些缺點羹令,大大提供性能。
一损痰、解決思路
? ? 提供兩種思路:hash與partition福侈。
1、hash方案
? ? hash就是rowkey前面由一串隨機字符串組成卢未,隨機字符串生成方式可以由SHA或者MD5方式生成肪凛,只要region所管理的start-end keys范圍比較隨機,那么就可以解決寫熱點問題尝丐。
散列函數(shù)是固定的显拜,讀取的時候,拿著未散列的數(shù)據(jù)爹袁,求出散列的數(shù)據(jù)远荠,即可實現(xiàn)查詢。
例如:
Java代碼
long?currentId?=?1L;??
byte?[]?rowkey?=?Bytes.add(MD5Hash.getMD5AsHex(Bytes.toBytes(currentId))??
????????????????????.substring(0,?8).getBytes(),Bytes.toBytes(currentId));??
? ? ?假如rowkey原本是自增長的long型失息,可以將rowkey轉(zhuǎn)為hash再轉(zhuǎn)為bytes譬淳,加上本身id轉(zhuǎn)為bytes,這樣就生成隨便的rowkey盹兢。那么對于這種方式的rowkey設(shè)計邻梆,如何去進行預(yù)分區(qū)呢?
取樣绎秒,先隨機生成一定數(shù)量的rowkey浦妄,將取樣數(shù)據(jù)按升序排序放到一個集合里。
根據(jù)預(yù)分區(qū)的region個數(shù),對整個集合平均分割剂娄,即是相關(guān)的splitkeys蠢涝。
HBaseAdmin.createTable(HTableDescriptor tableDescriptor,byte[][] splitkeys)可以指定預(yù)分區(qū)的splitkey,即指定region間的rowkey臨界值阅懦。
? ? 創(chuàng)建split計算器和二,用于從抽樣數(shù)據(jù)生成一個比較合適的splitkeys
Java代碼
public?class?HashChoreWoker?implements?SplitKeysCalculator{??
????//隨機取機數(shù)目??
????private?int?baseRecord;??
????//rowkey生成器??
????private?RowKeyGenerator?rkGen;??
????//取樣時,由取樣數(shù)目及region數(shù)相除所得的數(shù)量.??
????private?int?splitKeysBase;??
????//splitkeys個數(shù)??
????private?int?splitKeysNumber;??
????//由抽樣計算出來的splitkeys結(jié)果??
????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保存抽樣數(shù)據(jù)耳胎,已排序過??
????????TreeSet<byte[]>?rows?=?new?TreeSet<byte[]>(Bytes.BYTES_COMPARATOR);??
????????for?(int?i?=?0;?i?<?baseRecord;?i++)?{??
????????????rows.add(rkGen.nextId());??
????????}??
????????int?pointer?=?0;??
????????Iterator<byte[]>?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及實現(xiàn)
Java代碼
//interface??
public?interface?RowKeyGenerator?{??
????byte?[]?nextId();??
}??
//implements??
public?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測試
Java代碼
public?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();??
????}??
? ? ?查看建表結(jié)果惯吕,執(zhí)行:scan 'hbase:meta'
以上就是按照hash方式,預(yù)建好分區(qū)怕午,以后再插入數(shù)據(jù)的時候废登,也是按照此rowkeyGenerator的方式生成rowkey。
2诗轻、partition的方式
? ? partition顧名思義就是分區(qū)式钳宪,這種分區(qū)有點類似于mapreduce中的partitioner,將區(qū)域用長整數(shù)作為分區(qū)號扳炬,每個region管理著相應(yīng)的區(qū)域數(shù)據(jù)吏颖,在rowkey生成時,將ID取模后恨樟,然后拼上ID整體作為rowkey半醉,這個比較簡單,不需要取樣劝术,splitkeys也非常簡單缩多,直接是分區(qū)號即可。直接上代碼:
Java代碼
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的編號衬吆,測試類如下:
Java代碼
????public?void?testPartitionAndCreateTable()?throws?Exception{??
????????PartitionRowKeyManager?rkManager?=?new?PartitionRowKeyManager();??
????????//只預(yù)建10個分區(qū)??
????????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();??
????}??
? ?通過partition實現(xiàn)的loadblance寫的話,當(dāng)然生成rowkey方式也要結(jié)合當(dāng)前的region數(shù)目取模而求得绳泉,大家同樣也可以做些實驗逊抡,看看數(shù)據(jù)插入后的分布。
? ? ?在這里也順提一下零酪,如果是順序的增長型原id,可以將id保存到一個數(shù)據(jù)庫冒嫡,傳統(tǒng)的也好,redis的也好,每次取的時候四苇,將數(shù)值設(shè)大1000左右孝凌,以后id可以在內(nèi)存內(nèi)增長,當(dāng)內(nèi)存數(shù)量已經(jīng)超過1000的話月腋,再去load下一個蟀架,有點類似于oracle中的sqeuence.
? ? ?隨機分布加預(yù)分區(qū)也不是一勞永逸的瓣赂。因為數(shù)據(jù)是不斷地增長的,隨著時間不斷地推移辜窑,已經(jīng)分好的區(qū)域钩述,或許已經(jīng)裝不住更多的數(shù)據(jù)寨躁,當(dāng)然就要進一步進行split了穆碎,同樣也會出現(xiàn)性能損耗問題,所以我們還是要規(guī)劃好數(shù)據(jù)增長速率职恳,觀察好數(shù)據(jù)定期維護所禀,按需分析是否要進一步分行手工將分區(qū)再分好,也或者是更嚴重的是新建表放钦,做好更大的預(yù)分區(qū)然后進行數(shù)據(jù)遷移色徘。如果數(shù)據(jù)裝不住了,對于partition方式預(yù)分區(qū)的話操禀,如果讓它自然分裂的話褂策,情況分嚴重一點。因為分裂出來的分區(qū)號會是一樣的颓屑,所以計算到partitionId的話斤寂,其實還是回到了順序?qū)懩甏瑫胁糠譄狳c寫問題出現(xiàn)揪惦,如果使用partition方式生成主鍵的話遍搞,數(shù)據(jù)增長后就要不斷地調(diào)整分區(qū)了,比如增多預(yù)分區(qū)器腋,或者加入子分區(qū)號的處理.(我們的分區(qū)號為long型溪猿,可以將它作為多級partition)
? ? 以上基本已經(jīng)講完了防止熱點寫使用的方法和防止頻繁split而采取的預(yù)分區(qū)。但rowkey設(shè)計纫塌,遠遠也不止這些诊县,比如rowkey長度,然后它的長度最大可以為char的MAXVALUE,但是看過之前我寫KeyValue的分析知道措左,我們的數(shù)據(jù)都是以KeyValue方式存儲在MemStore或者HFile中的依痊,每個KeyValue都會存儲rowKey的信息,如果rowkey太大的話媳荒,比如是128個字節(jié)抗悍,一行10個字段的表,100萬行記錄钳枕,光rowkey就占了1.2G+所以長度還是不要過長缴渊,另外設(shè)計,還是按需求來吧鱼炒。