轉自:http://www.cnblogs.com/bdifn/p/3801737.html
問題導讀:
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<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及實現 //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測試
@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(); }
查看建表結果:執行 scan 'hbase:meta'
以上我們只是顯示了部分region的信息,可以看到region的start-end key 還是比較隨機散列的。同樣可以查看hdfs的目錄結構,的確和預期的38個預分區一致:
以上,就已經按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表時有一個參考,但是遺憾的是,由於時間關系,我只花了一點業余時間簡單搭了一下框架,目前沒有更一步的實現,以后有時間再加以完善,當然也歡迎大家一起加入,一起學習吧。
項目使用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:100000000 Execution 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
原貼地址:http://www.cnblogs.com/bdifn/p/3801737.html ,轉載請注明