hbase-region split剖析

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

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

    今天的分享就到这,有看不明白的地方一定是我写的不够清楚,所有欢迎提任何问题以及改善方法。

你可能感兴趣的:(hbase-region split剖析)