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操作的流程如下图所示:

hbase手动region个数建议 hbase手动split语句_ci

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'}}