hbase region 切分是hbases水平扩展一个重要因素,将一个region切分为两个小region,并将切分后的region放在不同的节点上,以达到将负载进行均衡到其他节点。下面从split的策略、split流程以及split策略的设置三方面进行讲解region split。
split策略
region split的策略分为如下几种DisabledRegionSplitPolicy、ConstantSizeRegionSplitPolicy、IncreasingToUpperBoundRegionSplitPolicy、DelimitedKeyPrefixRegionSplitPolicy 、KeyPrefixRegionSplitPolicy以及SteppingSplitPolicy。本节将从split的触发条件、split切分的切分点以及相应核心源码三个方面进行讲解。
DisabledRegionSplitPolicy
禁用split策略,使用该策略,region不会进行自动切分,可以进行人为手动的切分。
ConstantSizeRegionSplitPolicy
固定大小自动split策略,在hbase 0.94.0之前使用的默认策略 。
split触发条件
当region中存在一个store的大小大于desiredMaxFileSize值则触发split。
当创建表时指定了MAX_FILESIZE属性,则desiredMaxFileSize = MAX_FILESIZE * (1+浮动率),浮动率由配置hbase.hregion.max.filesize.jitter计算得出。
当创建表时未指定MAX_FILESIZE属性,则desiredMaxFileSize = hbase.hregion.max.filesize*1+浮动率),浮动率由配置hbase.hregion.max.filesize.jitter计算得出。
核心触发条件源码如下:
protected boolean shouldSplit() {
boolean force = region.shouldForceSplit();
boolean foundABigStore = false;
//(1)逐个计算region中的store是否有大于desiredMaxFileSize的。
for (Store store : region.getStores()) {
if ((!store.canSplit())) {
return false;
}
if (store.getSize() > desiredMaxFileSize) {
foundABigStore = true;
}
}
return foundABigStore || force;
}
//desiredMaxFileSize的赋值逻辑
protected void configureForRegion(HRegion region) {
super.configureForRegion(region);
Configuration conf = getConf();
//(1)当表指定了MAX_FILESIZE属性,则desiredMaxFileSize 为该属性的值
HTableDescriptor desc = region.getTableDesc();
if (desc != null) {
this.desiredMaxFileSize = desc.getMaxFileSize();
}
//(2) 表未指定MAX_FILESIZE属性,则desiredMaxFileSize = hbase.hregion.max.filesize配置的值
if (this.desiredMaxFileSize <= 0) {
this.desiredMaxFileSize = conf.getLong(HConstants.HREGION_MAX_FILESIZE,
HConstants.DEFAULT_MAX_FILE_SIZE);
}
//(3)将desiredMaxFileSize 乘以浮动系数。
double jitter = conf.getDouble("hbase.hregion.max.filesize.jitter", 0.25D);
this.jitterRate = (RANDOM.nextFloat() - 0.5D) * jitter;
long jitterValue = (long) (this.desiredMaxFileSize * this.jitterRate);
// make sure the long value won't overflow with jitter
if (this.jitterRate > 0 && jitterValue > (Long.MAX_VALUE - this.desiredMaxFileSize)) {
this.desiredMaxFileSize = Long.MAX_VALUE;
} else {
this.desiredMaxFileSize += jitterValue;
}
}
split触发点计算
本策略的切分点为该region上最大的store中最大的hfile的中间block的startkey作为splitpoint,如果该splitpoint等于该hfile的startkey或等于endkey则不进行分割。
核心源码如下:
protected byte[] getSplitPoint() {
//(1)是否有指定切分点,指定了切分点则使用指定的切分点进行切分。
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) {
//(2)获取该store中最大的hfile文件,选取该hfile的midkey作为splitpoint,如果midkey等于该hfile的startkey或等于endkey则返回null。
byte[] splitPoint = s.getSplitPoint();
long storeSize = s.getSize();
if (splitPoint != null && largestStoreSize < storeSize) {
splitPointFromLargestStore = splitPoint;
largestStoreSize = storeSize;
}
}
return splitPointFromLargestStore;
}
public final byte[] getSplitPoint() throws IOException {
if (this.storefiles.isEmpty()) {
return null;
}
//(1)获取store上最大storefile,获取该storefile的中间key作为切分点
return StoreUtils.getLargestFile(this.storefiles).getFileSplitPoint(this.kvComparator);
}
byte[] getFileSplitPoint(KVComparator comparator) throws IOException {
if (this.reader == null) {
LOG.warn("Storefile " + this + " Reader is null; cannot get split point");
return null;
}
//(1)获取该hfile的中间block的startkey作为midkey,由于并不会遍历block,只是从block的元数据中获取startkey,所以这个过程基本不耗时。
byte [] midkey = this.reader.midkey();
//(2)判断获取的midkey是否与该hfile的startkey或endkey相同,相同则返回null即不进行切分。
if (midkey != null) {
KeyValue mk = KeyValue.createKeyValueFromKey(midkey, 0, midkey.length);
byte [] fk = this.reader.getFirstKey();
KeyValue firstKey = KeyValue.createKeyValueFromKey(fk, 0, fk.length);
byte [] lk = this.reader.getLastKey();
KeyValue lastKey = KeyValue.createKeyValueFromKey(lk, 0, lk.length);
if (comparator.compareRows(mk, firstKey) == 0 || comparator.compareRows(mk, lastKey) == 0) {
if (LOG.isDebugEnabled()) {
LOG.debug("cannot split because midkey is the same as first or last row");
}
return null;
}
return mk.getRow();
}
return null;
}
IncreasingToUpperBoundRegionSplitPolicy
动态调整触发切分大小策略,从hbase0.94.0开始默认的自动切分策略,继承自ConstantSizeRegionSplitPolicy。
split触发条件
一:指定表在当前region对应的regionserver上的region个数等于0或大于100,则返回desiredMaxFileSize作为触发大小。
二:指定表在当前region对应的regionserver上的region个数大于0并且小于100,则动态修改split触发的大小,计算公式: min(desiredMaxFileSize,initialSize*tableregioncount*tableregioncount*tableregioncount)
1)initialSize(初始大小):
如果配置文件指定了hbase.increasing.policy.initial.size,则使用该值。
如果配置文件未指定hbase.increasing.policy.initial.size,则使用创建表时指定的MEMSTORE_FLUSHSIZE属性值*2,未指定表的MEMSTORE_FLUSHSIZE属性则使用hbase.hregion.memstore.flush.size*2。
2)tableregioncount:指定表在当前region对应的regionserver上有多少个region。
例如:
前提:
1.分割之后的子region任然在父region的regionserver上
2.未指定hbase.increasing.policy.initial.size和表属性MEMSTORE_FLUSHSIZE,指定hbase.hregion.memstore.flush.size为256M。
3.desiredMaxFileSize为10G
第一次分割的触发大小:256*2*1*1*1 = 256M
第二次分割的触发大小:256*2*2*2*2 = 2048M
第三次分割的触发大小:256*2*4*4*4 =16384M >desiredMaxFileSize(10G),取desiredMaxFileSize(10G)
之后所有分割的触发大小都是desiredMaxFileSize(10G)。
核心源码如下:
protected boolean shouldSplit() {
boolean force = region.shouldForceSplit();
boolean foundABigStore = false;
// (1)获取指定表在当前region对应的regionserver上的region个数
int tableRegionsCount = getCountOfCommonTableRegions();
// (2)获取检查大小
long sizeToCheck = getSizeToCheck(tableRegionsCount);
for (Store store : region.getStores()) {
if (!store.canSplit()) {
return false;
}
// Mark if any store is big enough
long size = store.getSize();
if (size > sizeToCheck) {
LOG.debug("ShouldSplit because " + store.getColumnFamilyName() + " size=" + size
+ ", sizeToCheck=" + sizeToCheck + ", regionsWithCommonTable="
+ tableRegionsCount);
foundABigStore = true;
}
}
return foundABigStore | force;
}
//计算check size
protected long getSizeToCheck(final int tableRegionsCount) {// (1)region个数等于0或大于100,则返回desiredMaxFileSize作为触发大小
// (2)region个数大于0并且小于100,则动态修改split触发的大小,计算公式: min(desiredMaxFileSize,initialSize*tableregioncount*tableregioncount*tableregioncount)
return tableRegionsCount == 0 || tableRegionsCount > 100
? getDesiredMaxFileSize()
: Math.min(getDesiredMaxFileSize(),
initialSize * tableRegionsCount * tableRegionsCount * tableRegionsCount);
}
split触发点计算
和ConstantSizeRegionSplitPolicy策略的split触发点计算一致。
KeyPrefixRegionSplitPolicy
根据key的前缀进行切分,继承自IncreasingToUpperBoundRegionSplitPolicy。
split触发条件
split触发条件和IncreasingToUpperBoundRegionSplitPolicy一致。
split触发点计算
根据rowKey的指定长度的前缀对数据进行分组,以便于将这些数据分到相同的Region中,长度指定由创建表时的KeyPrefixRegionSplitPolicy.prefix_length属性指定(旧版本的属性为:prefix_split_key_policy.prefix_length) 比如rowKey都是8位的,指定前3位是前缀,那么前3位相同的rowKey在进行region split的时候会分到相同的region中。
核心源码如下:
protected byte[] getSplitPoint() {
//(1)调用父类的方法,获取该region上最大的store中最大的hfile的中间block的startkey作为splitpoint。
byte[] splitPoint = super.getSplitPoint();
//(2)指定的前缀长度大于0,则获取splitPoint的指定长度作为最后split的切分点。
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;
}
}
//获取前缀长度
protected void configureForRegion(HRegion region) {super.configureForRegion(region);
prefixLength = 0;
//(1)获取表的KeyPrefixRegionSplitPolicy.prefix_length属性为前缀长度,旧版的为prefix_split_key_policy.prefix_length属性。
String prefixLengthString = region.getTableDesc().getValue(
PREFIX_LENGTH_KEY);
if (prefixLengthString == null) {
prefixLengthString = region.getTableDesc().getValue(PREFIX_LENGTH_KEY_DEPRECATED);
if (prefixLengthString == null) {
return;
}
}
try {
prefixLength = Integer.parseInt(prefixLengthString);
} catch (NumberFormatException nfe) {
return;
}
......
}
DelimitedKeyPrefixRegionSplitPolicy
根据分割符获取前缀进行切分,继承自IncreasingToUpperBoundRegionSplitPolicy。
split触发条件
split触发条件和IncreasingToUpperBoundRegionSplitPolicy一致。
split触发点计算
与KeyPrefixRegionSplitPolicy的split触发点计算类似也是使用rowkey的前缀作为splitpoint,不同点在于KeyPrefixRegionSplitPolicy使用固定长度作为前缀,而DelimitedKeyPrefixRegionSplitPolicy指定分隔字符进行拆分作为前缀。分隔字符由表的DelimitedKeyPrefixRegionSplitPolicy.delimiter属性指定。
核心源码如下:
protected byte[] getSplitPoint() {
//(1)调用父类的方法,获取该region上最大的store中最大的hfile的中间block的startkey作为splitpoint。
byte[] splitPoint = super.getSplitPoint();
//(2)获取分隔符之前的字符作为splitpoint。
if (splitPoint != null && delimiter != null) {
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;
}
return Arrays.copyOf(splitPoint, Math.min(index, splitPoint.length));
} else {
return splitPoint;
}
}
//获取分隔符号
protected void configureForRegion(HRegion region) {super.configureForRegion(region);
//(1)获取表的DelimitedKeyPrefixRegionSplitPolicy.delimiter属性作为分隔符
String delimiterString = region.getTableDesc().getValue(DELIMITER_KEY);
if (delimiterString == null || delimiterString.length() == 0) {
LOG.error(DELIMITER_KEY + " not specified for table " + region.getTableDesc().getTableName() +
". Using default RegionSplitPolicy");
return;
}
delimiter = Bytes.toBytes(delimiterString);}
SteppingSplitPolicy
分阶段进行固定大小分割,继承自IncreasingToUpperBoundRegionSplitPolicy。
split触发条件
当只有一个region的时候,使用initialSize作为触发split大小,否则使用desiredMaxFileSize作为触发split大小。initialSize和desiredMaxFileSize都在前面进行过描述。
核心代码如下:
protected long getSizeToCheck(final int tableRegionsCount) {
return tableRegionsCount == 1 ? this.initialSize : getDesiredMaxFileSize();
}
split触发点计算
和ConstantSizeRegionSplitPolicy策略的split触发点计算一致。
split流程
当split发生的时候,创建的子region并不会马上把所有的数据写入新的文件,而是创建一个小的链接引用文件指向分割点的头部和尾部。这些引用文件会在compactions操作逐渐被清除。只有当region没有引用文件的时候才可以进行split操作。
regionserver在split开始和结束都会通知hmaster去更新.META表,使客户端可以知道新的子region,重新组织hdfs上的文件路径。
split操作的流程如下图所示:
1.在zookeeper的/hbase/region-in-transition/region-name路径下创建znode并标记状态为SPLITTING.。
2.hmaster监听/hbase/region-in-transition/region-name路径得知该region正在进行split
3.regionserver在hdfs的父region路径下创建.splits路径
4.regionserver上关闭父region,此时父region为offline,当有客户端访问该父region时会报NotServingRegionException错误。
5.在hdfs的.splits路径下创建子region A、B的路径,然后split,其实就是在子region A、B的路径下创建引用文件指向父region的文件。
6.创建实际的子region路径(上面创建的文件都是在父region路径下),并把引用文件移动到该路径下。
7.该regionserver向拥有.META表的regionserver发送一条put请求,修改该spliting region的状态offline,并且添加子region的regionname。在这个时候并没有单独的子region信息,当客户端scan表.META时知道到父region在split,但是不知道子region的信息。当put请求成功后父region会进行快速的split。
8.该regionserver并发的打开两个子region。
9.该regionserver将两个子region的信息(host)发送到拥有.META表的regionserver,添加到.META表中。这时两个子region上线,客户端可以知道这两个子region并向这两个子region发送请求。客户端会缓存.META表中的数据,当使用缓存中的数据进行访问regionserver时出现问题,客户端会重新请求.META表中的内容进行缓存。
10.将步骤1创建的znode,将该状态转为split,这时split操作完成,hmaster得知split操作完成。
11.完成上述步骤后,hdfs仍然包含引用文件指向父region,这些引用文件会在子region进行compactions时进行移除。hmaster中的gc任务会周期的检查子region是否还有引用父region的文件,没有的话会将父region进行移除。
split策略设置
分为两种方式进行设置
1)全局方式,通过修改配置文件中的hbase.regionserver.region.split.policy属性进行指定策略,未指定策略的表都使用该配置指定的策略。
2)表级别,通过指定表的属性进行指定split策略,hbase shell中案例如下:
create 'test1', { NAME => 'cf',COMPRESSION => 'GZ'}, {METADATA => {'SPLIT_POLICY' => 'org.apache.hadoop.hbase.regionserver.ConstantSizeRegionSplitPolicy',MAX_FILESIZE=> '10737418240'}}
今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。