HBase预分区设计
?
?
? ? ? HBase中,表會(huì)被劃分為1...n個(gè)Region,被托管在RegionServer中。Region二個(gè)重要的屬性:StartKey與 EndKey表示這個(gè)Region維護(hù)的rowKey范圍,當(dāng)我們要讀/寫數(shù)據(jù)時(shí),如果rowKey落在某個(gè)start-end key范圍內(nèi),那么就會(huì)定位到目標(biāo)region并且讀/寫到相關(guān)的數(shù)據(jù)。簡(jiǎn)單地說(shuō),有那么一點(diǎn)點(diǎn)類似人群劃分,1-15歲為小朋友,16-39歲為年輕 人,40-64為中年人,65歲以上為老年人。(這些數(shù)值都是拍腦袋出來(lái)的,只是舉例,非真實(shí)),然后某人找隊(duì)伍,然后根據(jù)年齡,處于哪個(gè)范圍,就找到它 所屬的隊(duì)伍。 : ( 有點(diǎn)廢話了。。。。
? ? 然后,默認(rèn)地,當(dāng)我們只是通過(guò)HBaseAdmin指定TableDescriptor來(lái)創(chuàng)建一張表時(shí),只有一個(gè)region,正處于混沌時(shí) 期,start-end key無(wú)邊界,可謂海納百川。啥樣的rowKey都可以接受,都往這個(gè)region里裝,然而,當(dāng)數(shù)據(jù)越來(lái)越多,region的size越來(lái)越大時(shí),大到 一定的閥值,hbase認(rèn)為再往這個(gè)region里塞數(shù)據(jù)已經(jīng)不合適了,就會(huì)找到一個(gè)midKey將region一分為二,成為2個(gè)region,這個(gè)過(guò) 程稱為分裂(region-split).而midKey則為這二個(gè)region的臨界,左為N無(wú)下界,右為M無(wú)上界。< midKey則為陰被塞到N區(qū),> midKey則會(huì)被塞到M區(qū)。
? ? 如何找到midKey?涉及的內(nèi)容比較多,暫且不去討論,最簡(jiǎn)單的可以認(rèn)為是region的總行數(shù) / 2 的那一行數(shù)據(jù)的rowKey.雖然實(shí)際上比它會(huì)稍復(fù)雜點(diǎn)。
? ? 如果我們就這樣默認(rèn)地,建表,表里不斷地Put數(shù)據(jù),更嚴(yán)重的是我們的rowkey還是順序增大的,是比較可怕的。存在的缺點(diǎn)比較明顯。
? ? 首先是熱點(diǎn)寫,我們總是會(huì)往最大的start-key所在的region寫東西,因?yàn)槲覀兊膔owkey總是會(huì)比之前的大,并且hbase的是按升序方式排序的。所以寫操作總是被定位到無(wú)上界的那個(gè)region中。
? ? 其次,由于寫熱點(diǎn),我們總是往最大start-key的region寫記錄,之前分裂出來(lái)的region不會(huì)再被寫數(shù)據(jù),有點(diǎn)被打進(jìn)冷宮的趕腳,它們都處于半滿狀態(tài),這樣的分布也是不利的。
? ? 如果在寫比較頻率的場(chǎng)景下,數(shù)據(jù)增長(zhǎng)快,split的次數(shù)也會(huì)增多,由于split是比較耗時(shí)耗資源的,所以我們并不希望這種事情經(jīng)常發(fā)生。
? ? ............
? ? 看到這些缺點(diǎn),我們知道,在集群的環(huán)境中,為了得到更好的并行性,我們希望有好的load blance,讓每個(gè)節(jié)點(diǎn)提供的請(qǐng)求處理都是均等的。我們也希望,region不要經(jīng)常split,因?yàn)閟plit會(huì)使server有一段時(shí)間的停頓,如何能做到呢?
隨機(jī)散列與預(yù)分區(qū)。二者結(jié)合起來(lái),是比較完美的,預(yù)分區(qū)一開始就預(yù)建好了一部分region,這些region都維護(hù)著自已的start-end keys,再配合上隨機(jī)散列,寫數(shù)據(jù)能均等地命中這些預(yù)建的region,就能解決上面的那些缺點(diǎn),大大地提高了性能。
提供2種思路: hash 與 partition.
? ? 一、hash就是rowkey前面由一串隨機(jī)字符串組成,隨機(jī)字符串生成方式可以由SHA或者M(jìn)D5等方式生成,只要region所管理的start-end keys范圍比較隨機(jī),那么就可以解決寫熱點(diǎn)問(wèn)題。
?
假設(shè)rowKey原本是自增長(zhǎng)的long型,可以將rowkey轉(zhuǎn)為hash再轉(zhuǎn)為bytes,加上本身id 轉(zhuǎn)為bytes,組成rowkey,這樣就生成隨便的rowkey。那么對(duì)于這種方式的rowkey設(shè)計(jì),如何去進(jìn)行預(yù)分區(qū)呢?
? ? 1.取樣,先隨機(jī)生成一定數(shù)量的rowkey,將取樣數(shù)據(jù)按升序排序放到一個(gè)集合里
? ? 2.根據(jù)預(yù)分區(qū)的region個(gè)數(shù),對(duì)整個(gè)集合平均分割,即是相關(guān)的splitKeys.
? ? 3.HBaseAdmin.createTable(HTableDescriptor tableDescriptor,byte[][] splitkeys)可以指定預(yù)分區(qū)的splitKey,即是指定region間的rowkey臨界值.
?
?1.創(chuàng)建split計(jì)算器,用于從抽樣數(shù)據(jù)中生成一個(gè)比較合適的splitKeys
public class HashChoreWoker implements SplitKeysCalculator{//隨機(jī)取機(jī)數(shù)目private int baseRecord;//rowkey生成器private RowKeyGenerator rkGen;//取樣時(shí),由取樣數(shù)目及region數(shù)相除所得的數(shù)量.private int splitKeysBase;//splitkeys個(gè)數(shù)private int splitKeysNumber;//由抽樣計(jì)算出來(lái)的splitkeys結(jié)果private byte[][] splitKeys;public HashChoreWoker(int baseRecord, int prepareRegions) {this.baseRecord = baseRecord;//實(shí)例化rowkey生成器rkGen = new HashRowKeyGenerator();splitKeysNumber = prepareRegions - 1;splitKeysBase = baseRecord / prepareRegions;}public byte[][] calcSplitKeys() {splitKeys = new byte[splitKeysNumber][];//使用treeset保存抽樣數(shù)據(jù),已排序過(guò)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及實(shí)現(xiàn) //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++;}} }?
?
@Test 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();}?
? ? ? ?以上,就已經(jīng)按hash方式,預(yù)建好了分區(qū),以后在插入數(shù)據(jù)的時(shí)候,也要按照此rowkeyGenerator的方式生成rowkey,有興趣的話,也可以做些試驗(yàn),插入些數(shù)據(jù),看看數(shù)據(jù)的分布。
?
?
?
? ? ? ?二、partition故名思義,就是分區(qū)式,這種分區(qū)有點(diǎn)類似于mapreduce中的partitioner,將區(qū)域用長(zhǎng)整數(shù)(Long)作為分區(qū)號(hào),每 個(gè)region管理著相應(yīng)的區(qū)域數(shù)據(jù),在rowKey生成時(shí),將id取模后,然后拼上id整體作為rowKey.這個(gè)比較簡(jiǎn)單,不需要取 樣,splitKeys也非常簡(jiǎn)單,直接是分區(qū)號(hào)即可。直接上代碼吧:
?
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的編號(hào),我們看看測(cè)試類:
@Testpublic void testPartitionAndCreateTable() throws Exception{PartitionRowKeyManager rkManager = new PartitionRowKeyManager();//只預(yù)建10個(gè)分區(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();}?
? ? ? ?通過(guò)partition實(shí)現(xiàn)的loadblance寫的話,當(dāng)然生成rowkey方式也要結(jié)合當(dāng)前的region數(shù)目取模而求得,大家同樣也可以做些實(shí)驗(yàn),看看數(shù)據(jù)插入后的分布。
在這里也順提一下,如果是順序的增長(zhǎng)型原id,可以將id保存到一個(gè)數(shù)據(jù)庫(kù),傳統(tǒng)的也好,redis的也好,每次取的時(shí)候,將數(shù)值設(shè)大1000左右,以后 id可以在內(nèi)存內(nèi)增長(zhǎng),當(dāng)內(nèi)存數(shù)量已經(jīng)超過(guò)1000的話,再去load下一個(gè),有點(diǎn)類似于oracle中的sqeuence.
?
? ? ? ? 隨機(jī)分布加預(yù)分區(qū)也不是一勞永逸的。因?yàn)閿?shù)據(jù)是不斷地增長(zhǎng)的,隨著時(shí)間不斷地推移,已經(jīng)分好的區(qū)域,或許已經(jīng)裝不住更多的數(shù)據(jù),當(dāng)然就要進(jìn)一步進(jìn)行 split了,同樣也會(huì)出現(xiàn)性能損耗問(wèn)題,所以我們還是要規(guī)劃好數(shù)據(jù)增長(zhǎng)速率,觀察好數(shù)據(jù)定期維護(hù),按需分析是否要進(jìn)一步分行手工將分區(qū)再分好,也或者是 更嚴(yán)重的是新建表,做好更大的預(yù)分區(qū)然后進(jìn)行數(shù)據(jù)遷移。
?
總結(jié)
以上是生活随笔為你收集整理的HBase预分区设计的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Hybrid App
- 下一篇: 鸿蒙大陆7.1正式版隐藏英雄,守塔V7.