HBase中的SplitRegionPolicy實現原理及其原始碼解讀
我的HBase是使用的是CDH5.15中的版本,其版本對應的是HBase的版本為1.2,後續的分析都是基於該版本的原始碼做的分析。
一、SplitRegionPolicy及其子類介紹
在HBase的1.2版本中,RegionSplitPolicy的實現子類共有6個,如下類圖:
以下針對這幾個拆分策略做單獨的說明。
1、RegionSplitPolicy
RegionSplitPolicy是一個抽象類,其做為所有Region拆分策略的父類。在0.94版本以前,預設的拆分策略是ConstantSizeRegionSplitPolicy,在0.94版本以後,預設的拆分策略為IncreasingToUpperBoundRegionSplitPolicy,這個在RegionSplitPolicy的類註釋也有說明:
在RegionSplitPolicy中,需要重點關注一個方法getSplitPoint(),其返回Region分裂點的邏輯,其實現程式碼如下:
/** * @return the key at which the region should be split, or null * if it cannot be split. This will only be called if shouldSplit * previously returned true. */ protected byte[] getSplitPoint() { byte[] explicitSplitPoint = this.region.getExplicitSplitPoint(); if (explicitSplitPoint != null) { return explicitSplitPoint; } List<Store> stores = region.getStores(); byte[] splitPointFromLargestStore = null; long largestStoreSize = 0; for (Store s : stores) { byte[] splitPoint = s.getSplitPoint(); long storeSize = s.getSize(); if (splitPoint != null && largestStoreSize < storeSize) { splitPointFromLargestStore = splitPoint; largestStoreSize = storeSize; } } return splitPointFromLargestStore; }
其首先是判斷該Resion是否有使用者顯示定義的分裂點,如果有則使用使用者定義的分裂點,如果則沒有則取當前Region的Store中Size最大的那個定義的分裂點。
使用者通過在HBase Shell中建立表,通過SPLITS或者SPLITS_FILE引數指定,如下:
hbase>create 'test1','f1',SPLITS => ['10','20','30']
則其分裂點的分佈如下:
Region_Name Start_Key End_Key r1 10 r2 10 20 r3 20 30 r4 30
生成4個Regions。
如果分裂點比較多,不方便寫在命令列,可將其列到一個檔案中如splits.txt,每行寫一個分裂Key,如將上面的分裂Key寫到檔案中如下:
10
20
30
此時通過如下命令指定分裂Key:
hbase>create 'test1','f1',SPLITS_FILE=>'splits.txt'
2、IncreasingToUpperBoundRegionSplitPolicy
從上面的類圖也可以看出IncreasingToUpperBoundRegionSplitPolicy是ConstantSizeRegionSplitPolicy的子類,其優化了原來ConstantSizeRegionSplitPolicy只是單一按照Region檔案大小(通常預設為10G,其配置控制引數為hbase.hregion.max.filesize)的拆分策略,增加了對當前表的分片數做為判斷因子。如果表的分片數為0或者大於100,則切分大小還是以設定的單一Region檔案大小為標準;如果分片數在1~99之間,則取min(單一Region檔案大小 , Region增加策略的初使化大小(其可由配置控制引數為hbase.increasing.policy.initial.size指定;如果沒有配置該引數,由取值MemStore的快取重新整理值大小的兩倍,MemStore快取重新整理值預設其值為128M,即此時取值256M)* 當前Table Region數的3次方)的結果做為拆分控制大小。
確定initialSize大小的程式碼邏輯如下:
Configuration conf = getConf();
initialSize = conf.getLong("hbase.increasing.policy.initial.size", -1);
if (initialSize > 0) {
return;
}
HTableDescriptor desc = region.getTableDesc();
if (desc != null) {
initialSize = 2 * desc.getMemStoreFlushSize();
}
if (initialSize <= 0) {
initialSize = 2 * conf.getLong(HConstants.HREGION_MEMSTORE_FLUSH_SIZE,
HTableDescriptor.DEFAULT_MEMSTORE_FLUSH_SIZE);
}
確定其拆分控制大小的實現方法如下:
/**
* @return Region max size or {@code count of regions cubed * 2 * flushsize},
* which ever is smaller; guard against there being zero regions on this server.
*/
protected long getSizeToCheck(final int tableRegionsCount) {
// safety check for 100 to avoid numerical overflow in extreme cases
return tableRegionsCount == 0 || tableRegionsCount > 100
? getDesiredMaxFileSize()
: Math.min(getDesiredMaxFileSize(),
initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount);
}
要想達到每次拆分大小為10G的標準,則需要經過以下4次拆分:
第一次split:1^3 * 256 = 256MB
第二次split:2^3 * 256 = 2048MB
第三次split:3^3 * 256 = 6912MB
第四次split:4^3 * 256 = 16384MB > 10GB,因此取較小的值10GB
後面每次split的size都是10GB了
3、SteppingSplitPolicy
SteppingSplitPolicy是IncreasingToUpperBoundRegionSplitPolicy的子類,其總共原始碼只有幾行,如下:
public class SteppingSplitPolicy extends IncreasingToUpperBoundRegionSplitPolicy {
/**
* @return flushSize * 2 if there's exactly one region of the table in question
* found on this regionserver. Otherwise max file size.
* This allows a table to spread quickly across servers, while avoiding creating
* too many regions.
*/
protected long getSizeToCheck(final int tableRegionsCount) {
return tableRegionsCount == 1 ? this.initialSize : getDesiredMaxFileSize();
}
}
其對Region拆分檔案大小做了優化,如果只有1個Region的情況下,那第1次的拆分就是256M,後續則按配置的拆分檔案大小(10G)做為拆分標準。在IncreasingToUpperBoundRegionSplitPolicy策略中,針對大表的拆分表現很不錯,但是針對小表會產生過多的Region,SteppingSplitPolicy則將小表的Region控制在一個合理的範圍,對大表的拆分也不影響。
4、KeyPrefixRegionSplitPolicy
根據rowKey的字首對資料進行分組,以便於將這些資料分到相同的Region中,這裡是通過指定rowKey的前多少位作為字首做為拆分控制引數,其引數控制為通過指定Table的描述引數KeyPrefixRegionSplitPolicy.prefix_length(舊版為prefix_split_key_policy.prefix_length)控制拆分字首的長度,比如rowKey都是16位的,指定前5位是字首,那麼前5位相同的rowKey在進行region split的時候會分到相同的region中。
獲取拆分點的實現原碼如下:
@Override
protected byte[] getSplitPoint() {
byte[] splitPoint = super.getSplitPoint();
if (prefixLength > 0 && splitPoint != null && splitPoint.length > 0) {
// group split keys by a prefix
return Arrays.copyOf(splitPoint,
Math.min(prefixLength, splitPoint.length));
} else {
return splitPoint;
}
}
5、DelimitedKeyPrefixRegionSplitPolicy
DelimitedKeyPrefixRegionSplitPolicy和KeyPrefixRegionSplitPolicy要達到的結果類似,都是通過將Rowkey的部分字首做這拆分串,將其以這些字首前頭的RowKey,寫到相同的Region中;DelimitedKeyPrefixRegionSplitPolicy的實現方式和KeyPrefixRegionSplitPolicy通過指定字首固定長度的實現不同的是,其是根據RowKey中指定分隔字元做為拆分的,顯得更加靈活,如RowKey的值為“userid_eventtype_eventid”,且指定了分隔字串為下劃線"_",則DelimitedKeyPrefixRegionSplitPolicy將取RowKey值中從左往右且第一個分隔字串之前的字元做為拆分串,在該示例中就是“userid”。其實現程式碼如下:
@Override
protected byte[] getSplitPoint() {
byte[] splitPoint = super.getSplitPoint();
if (splitPoint != null && delimiter != null) {
//find the first occurrence of delimiter in split point
int index = com.google.common.primitives.Bytes.indexOf(splitPoint, delimiter);
if (index < 0) {
LOG.warn("Delimiter " + Bytes.toString(delimiter) + " not found for split key "
+ Bytes.toString(splitPoint));
return splitPoint;
}
// group split keys by a prefix
return Arrays.copyOf(splitPoint, Math.min(index, splitPoint.length));
} else {
return splitPoint;
}
}
6、DisabledRegionSplitPolicy
DisabledRegionSplitPolicy就是不使用Region拆分策略,將所有的資料都寫到同一個Region中,其實現非常簡單,程式碼如下:
public class DisabledRegionSplitPolicy extends RegionSplitPolicy {
@Override
protected boolean shouldSplit() {
return false;
}
}
HBase在執行Region拆分之前,都會呼叫該方法執行檢查是否可以拆分,如果不可以則不會執行後面的拆分點的檢查了。
二、RegionSplitPolicy的使用
Region拆分策略可以全域性統一配置,也可以為單獨的表指定拆分策略。
1、通過hbase-site.xml全域性統一配置
<property>
<name>hbase.regionserver.region.split.policy</name>
<value>org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy</value>
</property>
2、通過Java API為單獨的表指定Region拆分策略
HTableDescriptor tableDesc = new HTableDescriptor("test1");
tableDesc.setValue(HTableDescriptor.SPLIT_POLICY, IncreasingToUpperBoundRegionSplitPolicy.class.getName());
tableDesc.addFamily(new HColumnDescriptor(Bytes.toBytes("cf1")));
admin.createTable(tableDesc);
----
3、通過HBase Shell為單個表指定Region拆分策略
hbase> create 'test1', {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.IncreasingToUpperBoundRegionSplitPolicy'}},{NAME => 'cf1'}