HBase Rowkey的散列與預分區設計


轉自: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 ,轉載請注明


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM