HBase Region拆分逻辑

Region拆分

Region的拆分逻辑是通过CompactSplitThread线程的requestSplit方法来触发的,每当执行MemstoreFlush操作时都会调用该方法进行判断,看是否有必要对目标Region进行拆分。

拆分前提

Region可拆分的前提是需要满足如下约束条件:

  1. 目标RegionServer的线上Region数没有达到hbase.regionserver.regionSplitLimit参数阀值

    该参数的默认值为1000,达到900时RegionServer将会打印警告:Total number of regions is approaching the upper limit。

  2. StoreFile文件数量没有达到hbase.hstore.blockingStoreFiles参数阀值

    此时会优先触发整理操作(详细参考Region整理章节)

  3. 要拆分的Region不属于meta表格

    HBase不支持对meta表格中的Region做拆分操作。

  4. Region没有处于recovering状态(参考Region恢复章节)

  5. 由具体的拆分策略来做决定,通过其shouldSplit方法

    HBase对外提供了5中拆分策略,全部由RegionSplitPolicy派生而来,5中策略分别为:

    • ConstantSizeRegionSplitPolicy

      当Region中某个Store数据量达到hbase.hregion.max.filesize参数阀值时进行拆分

    • IncreasingToUpperBoundRegionSplitPolicy

      当Region中某个Store数据量达到sizeToCheck阀值时进行拆分,sizeToCheck是通过如下方法计算得出的,如果RegionServer上含有指定Table的Region数量在(0,100]之间,返回如下公式的值:

      min(hbase.hregion.max.filesize, regionCount^3 * initialSize)

      否则返回hbase.hregion.max.filesize参数值。由此可见该拆分策略是增量式的,随着Region数量的增多,拆分阀值也逐渐越大,直至达到目标上限为止。

    • DelimitedKeyPrefixRegionSplitPolicy

      拆分策略如下:如果定义rowkey时,采用'_'作为字段分隔符(如:userid_eventtype_eventid),则采用该策略拆分之后,能够确保具有相同userid的记录隶属于同一Region。

      该类继承至IncreasingToUpperBoundRegionSplitPolicy,其拆分前提与父类相同,只是在拆分点的获取上略有不同(参考下一章节)。

    • KeyPrefixRegionSplitPolicy

      与DelimitedKeyPrefixRegionSplitPolicy类似,只不过是选取指定长度的字符前缀来作为row的分组。

    • DisabledRegionSplitPolicy

      该策略shouldSplit方法永远返回false,表示不启用拆分功能,不对Region做任何拆分。

获取拆分点

拆分点主要是通过调用拆分策略的getSplitPoint方法来进行获取,不同的拆分策略有着不同的获取逻辑,具体如下:

  1. 如果拆分点是用过通过split命令强制声明的,返回用户指定的拆分点。

  2. 如果采用的拆分策略为ConstantSizeRegionSplitPolicy或IncreasingToUpperBoundRegionSplitPolicy,则选取Region中数据总量最大的Store,通过其getSplitPoint方法来获取切分点。

    方法在执行过程中还需要考虑StoreFileManager的实现类型:

    • 如果是DefaultStoreFileManager

      从目标Store中选择一个数据量最大的StoreFile,对其执行getFileSplitPoint方法来获取midkey作为拆分点(从根索引中读取,参考HFile存储结构)。

    • 如果是StripeStoreFileManager(采用了StripeCompaction)

      如果Store中只有一个Stripe,获取数据量最大的StoreFile,对其执行getFileSplitPoint方法来获取midKey信息作为该Region的拆分点。

      如果有多个Stripe,则试图在所有Stripe中间找到如下图所示的分隔线,使分割线左右两侧的数据总量最为接近。

      分割线确立以后,如果能满足以下约束条件,便可直接确定该Region的拆分点。

      largeSideSize / smallSideSize < 1.5

      其中largeSideSize为数据总量偏大的一方(即图中分割线左边的数据总量),smallSideSize为数据总量偏小的一方(即图中分割线右边的数据总量),而1.5是通过hbase.store.stripe.region.split.max.imbalance配置项声明的。

      拿示例图片来做说明:由于largeSideSize(3072m)/smallSideSize(2560M)=1.2,数值低于1.5,因此直接将Stripe-2的endRow作为该Region的拆分点。而如果Stripe-2的大小为20480m,那么便没有办法满足以上约束条件,这时需要进行如下处理:

      将stripe-2的一半大小划分到右侧区域(即将分割线画到如图所示位置),然后重新生成radio值。

      largeSideSize(12800) / smallSideSize(11264) = 1.14

      如果新生成的radio值比之前的radio还要大,则放弃这种处理办法,依然采用之前的处理方式,否则从stripe-2中选择数据总量最大的StoreFile,对其执行getFileSplitPoint方法来获取midkey信息,并将其作为该Region的拆分点返回。

执行拆分

Region在拆分过程中,需要HMaster和RegionServer的共同参与,中间的协调工作通过Zookeeper来实现。针对每一个待拆分的Region,RegionServer端会创建/hbase/region-intransition/{regionName}拆分节点,节点内容为RegionTransition对象,对象由以下几部分信息构成:

  • eventType

    事件类型,这里为RS_ZK_REQUEST_REGION_SPLIT,Master端会对该事件类型进行捕获,并做出相应的回调处理。

  • regionName

    表示对哪个Region执行的转换操作。

  • serverName

    目标Region部署在哪台RegionServer上。

  • payload

    装载额外的信息用于处理目标事件,这里为子Region的HRegionInfo信息。

拆分节点创建成功以后,HMaster端会进行相应的回调处理,通过监听Zookeeper的目标节点路径(参考AssignmentManager类的handleRegion方法)。在回调处理中,首先从目标拆分节点中读取出RegionTransition对象,修改其eventType属性值为RS_ZK_REGION_SPLITTING,并将该对象重新赋予目标拆分节点。随后对参与拆分操作的Region状态进行修改,将待拆分的父Region标记成SPLITTING状态,将两个新生成的子Region标记为SPLITTING_NEW,通过RegionStates类的updateRegionState方法。

与此同时,RegionServer端在创建出拆分节点以后会进入循环等待状态(代码逻辑参考ZKSplitTransactionCoordination类的waitForSplitTransaction方法),直至其eventType属性值变为RS_ZK_REGION_SPLITTING,然后开始执行如下处理:

  1. 在待拆分Region目录下创建.splits子目录来作为拆分目录。

  2. 将待拆分的Region关闭,不再提供线上服务,通过HRegion的close方法。

  3. 对Region中的storeFile进行拆分。

    假设将目标Region拆分成Split-A(存储拆分点之前的数据)和Split-B(存储拆分点之后的数据),拆分规则如下:

    首先定义如下3个变量:

    (1)firstKey:用于表示StoreFile文件的起始key;

    (2)lastKey:用于表示StoreFile文件的结束key;

    (3)splitKey:用于表示Region的拆分点。

    • 如果StoreFile满足firstKey < splitKey < lastKey,将[firstKey,splitKey]区间上的数据赋予Split-A。

    • 如果StoreFile满足firstKey < lastKey < splitKey,将整个StoreFile赋予Region-a。

    • 如果StoreFile满足splitKey < firstKey < lastKey,将整个StoreFile赋予Region-b

    Tip

    在HBase中,拆分后的StoreFile子文件是通过Reference来进行描述的,其数据内容采用protobuf进行序列化,每个Reference文件在HDFS上的存储路径为path/to/table/parentRegion/.splits/childRegion/cf/storefile.parentRegion

    拆分完成以后需要将子Region从path/to/table/parentRegion/.splits/childRegion路径移动到path/to/table/路径下。

  4. 修改meta表中Region元数据信息,通过MetaTableAccessor类的splitRegion方法

    将子Region添加到meta表格中,并将父Region标记为splitting状态(新增splitA和splitB列信息,内容为子Region的HRegionInfo)。

    Tip

    从该步骤开始起,Region拆分操作将无法回滚,一旦出错,需要将RegionServer停掉以便ServerShutdownHandler可以对目标Region的元数据进行修复,具体可参考Region状态管理章节中有关状态修复内容的介绍。

  5. 将拆分后的子Region进行启动,通过HRegion的openHRegion方法

    启动后的子Region与父Region部署在同一个RegionServer上,同时由于拆分后的子Region内部含有Reference文件,因此需要对其执行整理操作(参考Region整理章节)。

RegionServer端完成子Region的启动之后,需要将启动结果通知到HMaster端,通知逻辑同样是借助于Zookeeper来实现的,首先从Zookeeper的目标拆分节点中获取RegionTransition对象,将其eventType属性值修改为RS_ZK_REGION_SPLIT。节点状态修改后,Master端会进行如下回调处理:

(1)将父Region标记成SPLIT状态,并对其进行下线处理;

(2)对拆分后的子Region进行上线,将其标记为OPEN状态;

(3)删除Zookeeper中的拆分节点。

至此,Region的拆分逻辑成功执行,子Region开始提供线上服务。

回滚操作

Region在拆分过程中是采用事务进行管理的,如果在拆分的过程中出现了异常,可以对事务进行回滚,从而避免脏数据的产生。事务逻辑主要是通过SplitTransaction类来封装的,该类的使用模版如下:

SplitTransaction st = new SplitTransaction(conf, parent, midKey);
if (!st.prepare()) return;
try {
   st.execute(server, services);
} catch (IOException ioe) {
   try {
      st.rollback(server, services);
      return;
   } catch (RuntimeException e) {
      myAbortable.abort("Failed split, abort");
   }
}
		

可以看到回滚逻辑主要通过rollback方法来实现,方法在执行过程中会进行如下处理:

  1. 首先获取当前Region的拆分进度,以及到达该进度之前都做了哪些工作。

    Region在拆分过程中大致会经历如下几个阶段(详细的拆分过程可参考上一章节):

    (1)STARTED - 拆分逻辑被触发;

    (2)PREPARED - 执行拆分前的准备工作;

    (3)BEFORE_PRE_SPLIT_HOOK - 调用协处理器进行拆分前的拦截处理;

    (4)AFTER_PRE_SPLIT_HOOK - 协处理器处理结束;

    (5)SET_SPLITTING - Zookeeper中创建出拆分节点;

    (6)CREATE_SPLIT_DIR - 拆分目录被生成;

    (7)CLOSED_PARENT_REGION - 父Region被关闭;

    (8)OFFLINED_PARENT - 父Region被下线,不再提供线上服务;

    (9)STARTED_REGION_A_CREATION - 第一个子Region被成功创建;

    (10)STARTED_REGION_B_CREATION - 第二个子Region被成功创建;

    (11)PONR - 回滚分界点,即该阶段之前的操作可以回滚;

    (12)OPENED_REGION_A - 第一个子Region被开启;

    (13)OPENED_REGION_B - 第二个子Region被开启;

    (14)BEFORE_POST_SPLIT_HOOK - 调用协处理器进行拆分后的拦截处理;

    (15)AFTER_POST_SPLIT_HOOK - 协处理器处理结束。

    每到达一个阶段,HBase都会创建JournalEntry实体来对当前的进度进行标记,并将该实体添加到journal集合中。这样,通过journal集合便可获知当前拆分进度已经进入到了哪个阶段,然后依次向前回溯对每个阶段进行回滚即可。需要注意的是如果拆分进度到达了PONR阶段(point-of-no-return),此时将无法在执行回滚操作,只能将RegionServer停掉以便ServerShutdownHandler对Region的元数据进行修复(参考Region状态管理章节中有关状态修复的介绍)。

    针对该情况RegionServer宕掉以后将会打印如下提示信息:

    Abort; we got an error after point-of-no-return

  2. 针对当前进度之前的所有操作进行回滚。

    回滚操作是通过不断迭代来完成的,每次回滚都是针对一个具体的阶段来进行。

    (1)如果回滚时处于SET_SPLITTING阶段,删除Zookeeper中的拆分节点;

    (2)如果回滚时处于CREATE_SPLIT_DIR阶段,清理父Region下的拆分目录;

    (3)如果回滚时处于CLOSED_PARENT_REGION阶段,重新对父Region执行初始化操作;

    (4)如果回滚时处于STARTED_REGION_A_CREATION阶段,清理第一个子Region的存储目录;

    (5)如果回滚时处于STARTED_REGION_B_CREATION阶段,清理第二个子Region的存储目录;

    (6)如果回滚时处于OFFLINED_PARENT阶段,重新将父Region上线;

    (7)如果回滚时处于PONR或以后的阶段,将RegionServer停掉。

http://blog.csdn.net/javaman_chen/article/details/48048315

猜你喜欢

转载自aoyouzi.iteye.com/blog/2296663
今日推荐