美文网首页我爱编程
HBase学习 - HRegion Split

HBase学习 - HRegion Split

作者: aaron1993 | 来源:发表于2017-07-23 12:42 被阅读0次

    本文基于hbase-1.3.0源码

    1. 前言

    本文主要介绍在cluster模式下(并且使用zookeeper协调)region发生split的整个过程。这其中会涉及到HMaster和HRegionSever。

    2. split发生的时机

    2.1 不能split的场景

    1. 当前region包含reference file(也就是当前region从另外一个region split出来之后,依然通过reference file持有父region hfile的一个区间(上或下半部分)) ,compact操作会导致reference file被删除,因此若干次minor compact或则一次major compact之后reference file就会被删除。
    2. 当前region正处在recovering状态。
    3. meta table以及namespace table的region不可以split。
    4. 未满足split policy(见2.2)
    5. 此外由于split之前需要获取table的read lock,所以此时如果在修改table的meta信息的话(持有write lock)也会导致split阻塞。
    6. 当前regionserver的region数量已经达到了上限。由hbase.regionserver.regionSplitLimit配置决定

    1,2,3,4都由HRegion#checkSplit检查,split需要一个midKey作为切分点,checkSplit返回值就是midKey。5下文会提到。

    2.2 split policy

    split policy决定了region 是否达到split条件,以及splitKey。表元数据'SPLIT_POLICY'配置policy策略。未配置的情况下使用hbase.regionserver.region.split.policy配置的值,默认使用IncreasingToUpperBoundRegionSplitPolicy策略。

    下面介绍的所有policy类都继承自RegionSplitPolicy, 它有两个重要的方法:1, shouldSplit返回true|false决定是否可以split; 2, getSplitPoint返回进行split的点(一个row值,将所有row划成两段)

    1. ConstantSizeRegionSplitPolicy

      这种策略下,只要HRegion的任何一个HStore的size达到一个固定大小之后,就会发生split。关于这个大小的设置遵循一下规则(大小为desiredMaxFileSize):

      1. 将desiredMaxFileSize设置为table的元数据MAX_FILESIZE的大小,没有该元数据就从配置文件中读取hbase.hregion.max.filesize的值(当前版本默认为10G)
      2. 1中的值不是一个严格的值,还有一个抖动hbase.hregion.max.filesize.jitter
    2. IncreasingToUpperBoundRegionSplitPolicy

      这种策略下,和1不同的是size的选取。这种策略size和当前RegionServer上属于表t1的region个数有关(假设个数Rn)。

      1. Rn =0 || Rn > 100,size的选取和1一样。
      2. 不符合1的情况下,此时有一个‘initialSize’. initialSize 的大小由hbase.increasing.policy.initial.size,如果该配置未设置或者设置成负数,则initialSize被设置为2 * MEMSTORE_FLUSHSIZE(这是table的元数据),如果table没有该元数据或则initialSize依然小于0, initialSize被设置为2 * "hbase.hregion.memstore.flush.size(该配置默认值为128M)
      3. 有了initialSize值后,size被设置为min(ConstantSizeRegionSplitPolicy规则下的size, Rn * Rn * Rn * intialSize)
    3. KeyPrefixRegionSplitPolicy

      这种策略直接继承至IncreasingToUpperBoundRegionSplitPolicy,所以判断是否应该split的条件和它一样,但是splitKey的选取不一样。

      这种策略中有一个‘prefixLength’, 有table元数据KeyPrefixRegionSplitPolicy.prefix_length决定。它在拿到splitKey之后,会截取splitKey的前prefixLength位作为新的prefixKey。例如原splitKey为"1234567890",截取5位变成"1234500000", 这样保证了比"1234500000"小的在一个region里,比他大的在另一个region里,也就是说拥有公共前缀的key是不会被split到不同的region中。

    4. DelimitedKeyPrefixRegionSplitPolicy

      这种策略继承IncreasingToUpperBoundRegionSplitPolicy,所以判断是否应该split的条件和它一样,但是splitKey的选取不一样。

      这种策略下有一个分割符‘delimiter’, 从table元数据DelimitedKeyPrefixRegionSplitPolicy.delimiter获取,它在拿到原始的splitKey之后,找到delimiter最早的出现位置,然后截取该位置之前的前缀作为新的splitKey。

    2.3 region split时机:

    1. 内存中的数据(HMemStore)flush到磁盘时,判断是否需要split。
    2. 用户通过api或者shell触发split(RsRpcServices#splitRegion)。
    3. compaction导致split。

    1,2,3都需要满足split policy.

    2.4 与split相关的配置参数

    下文都假设配置zookeeper.znode.parent采用了默认值‘hbase’,zookeeper上所有的node都会以此配置值作为根路径。

    1. zookeeper.znode.unassigned 默认值为“region-in-transition”。
      HMaster启动时会根据该配置的值在zookeeper上创建路径'/hbase/region-in-transition', 发生split的的HRegion会在此路径下创建一个以region name为名的新节点,HMaster 监听该节点,因此知道正在split的region。

    2. hbase.regionserver.regionSplitLimit,默认值1000

      它规定了一个region server上的最多容纳的region的个数,超过这个值就不会再split。

    3. hbase.regionserver.thread.split用来split的线程池个数。默认为1.

    3. split过程

    3.1 基本概念

    1. HRegionInfo

      记录的region信息有:

      1. tableName, 即region所属table
      2. startKey, region 其实row key, 第一个region为空
      3. regionId, region 创建时的timestamp
      4. replicaId, region备份的情况下,主region 为0,其他依次递增
      5. encodedName, 前4项的32位md5值
      以上5项构成了region name,下面是'.meta.'这张表中ROW 值(meta表row值即region name):
      t1,,1500458612081.a33bb6a3c49157ab43876540d083e5f1
      t1表名,startKey是空的,1500458612081是regionId,没有replicaId,即没有备份,后面是md5值。
      除上述以外,HRegionInfo还包括:
      endKey, 开区间,为空表示region是最后一个region
      split,是否region正在split
      offline,region是否下线,split之后的region应该下线
      
    2. region split过程中的状态

      public enum SplitTransactionPhase {
          /**
           * Started
           */
          STARTED,
          /**
           * Prepared
           */
          PREPARED,
          /**
           * Before preSplit coprocessor hook
           */
          BEFORE_PRE_SPLIT_HOOK,
          /**
           * After preSplit coprocessor hook
           */
          AFTER_PRE_SPLIT_HOOK,
          /**
           * Set region as in transition, set it into SPLITTING state.
           */
          SET_SPLITTING,
          /**
           * We created the temporary split data directory.
           */
          CREATE_SPLIT_DIR,
          /**
           * Closed the parent region.
           */
          CLOSED_PARENT_REGION,
          /**
           * The parent has been taken out of the server's online regions list.
           */
          OFFLINED_PARENT,
          /**
           * Started in on creation of the first daughter region.
           */
          STARTED_REGION_A_CREATION,
          /**
           * Started in on the creation of the second daughter region.
           */
          STARTED_REGION_B_CREATION,
          /**
           * Opened the first daughter region
           */
          OPENED_REGION_A,
          /**
           * Opened the second daughter region
           */
          OPENED_REGION_B,
          /**
           * Point of no return.
           * If we got here, then transaction is not recoverable other than by
           * crashing out the regionserver.
           */
          PONR,
          /**
           * Before postSplit coprocessor hook
           */
          BEFORE_POST_SPLIT_HOOK,
          /**
           * After postSplit coprocessor hook
           */
          AFTER_POST_SPLIT_HOOK,
          /**
           * Completed
           */
          COMPLETED
        }
      

      region split在hbase里作为一个事务处理,整个split过程由SplitTransactionImpl(实现了Runnable接口)包装,上面枚举了这一事务执行过程中会经历的状态变化。在PONR状态之前发生fail,都会回滚。

    3.2 HRegionServer端

    hbase学习 - HRegionServer启动一文2.2节中提到hregion server会启动CompactionSplitThread专门负责split,compact,merge。CompactionSplitThread有三个重载的requestSplit方法如下:

    1. public synchronized boolean requestSplit(final Region r) 
    2. public synchronized void requestSplit(final Region r, byte[] midKey)
    3. public synchronized void requestSplit(final Region r, byte[] midKey, User user) {
        if (midKey == null) {
          LOG.debug("Region " + r.getRegionInfo().getRegionNameAsString() +
            " not splittable because midkey=null");
          if (((HRegion)r).shouldForceSplit()) {
            ((HRegion)r).clearSplit();
          }
          return;
        }
        try {
          //splits线程池执行SplitRequest
          this.splits.execute(new SplitRequest(r, midKey, this.server, user));
          if (LOG.isDebugEnabled()) {
            LOG.debug("Split requested for " + r + ".  " + this);
          }
        } catch (RejectedExecutionException ree) {
          LOG.info("Could not execute split for " + r, ree);
        }
      }
    }
    这三个方法
    
    1调用2, 2调用3。 其中方法1在MemStoreFlusher和CompactSplitThread#compaction调用,他会使用split policy的checkSplit(返回null或则midKey)检查是否可以split。 方法3由RsRpcServices(rpc服务)调用,尽管3里面没有调用split policy的checkSplit,但是3的调用泽RsRpcService#splitRegion在传midKey参数时使用了checkSplit,因此不满足的policy时midKey为null,依然不能split。
    

    1. SplitRequest

    SplitRequest实现了Runnable接口,主要逻辑在其方法doSplitting中:

    private void doSplitting(User user) {
        boolean success = false;
        server.metricsRegionServer.incrSplitRequest();
        long startTime = EnvironmentEdgeManager.currentTime();
        // 真正的split的主要过程都封装在SplitTransactionImpl中完成,包含了split过程中的状态转移.
        // 参考3.2 -1 split中会出现的状态。
        SplitTransactionImpl st = new SplitTransactionImpl(parent, midKey);
        try {
          // 获取table lock,获取的是read lock,因此不会影响table的其他region的split,compact等操作
          // 但是由于任何试图修改table schema的操作需要获取write lock,因此会被阻塞。
          tableLock =    
                   server.getTableLockManager().readLock(parent.getTableDesc().getTableName()
              , "SPLIT_REGION:" + parent.getRegionInfo().getRegionNameAsString());
          try {
            tableLock.acquire();
          } catch (IOException ex) {
            tableLock = null;
            throw ex;
          }
    
          
          if (!st.prepare()) return;
          try {
            st.execute(this.server, this.server, user);
            success = true;
          } catch (Exception e) {
            if (this.server.isStopping() || this.server.isStopped()) {
              LOG.info(
                  "Skip rollback/cleanup of failed split of "
                      + parent.getRegionInfo().getRegionNameAsString() + " because server is"
                      + (this.server.isStopping() ? " stopping" : " stopped"), e);
              return;
            }
            if (e instanceof DroppedSnapshotException) {
              server.abort("Replay of WAL required. Forcing server shutdown", e);
              return;
            }
            try {
              LOG.info("Running rollback/cleanup of failed split of " +
                parent.getRegionInfo().getRegionNameAsString() + "; " + e.getMessage(), e);
              // split过程中出现错误,都会尝试rollback,具体细节将会在split过程讲完后讲解。
              if (st.rollback(this.server, this.server)) {
                LOG.info("Successful rollback of failed split of " +
                  parent.getRegionInfo().getRegionNameAsString());
              } else {
                this.server.abort("Abort; we got an error after point-of-no-return");
              }
            } catch (RuntimeException ee) {
              String msg = "Failed rollback of failed split of " +
                parent.getRegionInfo().getRegionNameAsString() + " -- aborting server";
              // If failed rollback, kill this server to avoid having a hole in table.
              LOG.info(msg, ee);
              this.server.abort(msg + " -- Cause: " + ee.getMessage());
            }
            return;
          }
        } catch (IOException ex) {
          LOG.error("Split failed " + this, RemoteExceptionHandler.checkIOException(ex));
          server.checkFileSystem();
        } finally {
          if (this.parent.getCoprocessorHost() != null) {
            try {
              this.parent.getCoprocessorHost().postCompleteSplit();
            } catch (IOException io) {
              LOG.error("Split failed " + this,
                  RemoteExceptionHandler.checkIOException(io));
            }
          }
          
          // rpc发起的split会设置forceSplit=true,完成split之后需要归位为false。
          if (parent.shouldForceSplit()) {
            parent.clearSplit();
          }
          //释放 read lock
          releaseTableLock();
          ...
      }
    

    核心逻辑在SplitTransactionImpl中完成,调用器prepare和execute完成。doSplitting中完成table的readlock获取和释放,以及出现异常后rollback。

    2. SplitTransactionImpl

    在SplitTransacntionImple中完成split全部过程,他有一些重要成员:

    • parent, 等待split的HRegion
    • hri_a, hri_b, parent分裂成这两个
    • currentPhase, 当前split所处状态,参考3.1 - 2,初始状态为STARTED。
    • private final List<JournalEntry> journal, 当前所有已经完成的状态列表
    • splitRow,以这个值降parent分裂成两个。

    上面1中代码,先是调用了SplitTransactionImpl#prepare主要完成一项工作:

    • 拿到splitKey,parent的startKey和endKey,创建出两个HRegionInfo实例hri_a, hri_b,它们分别拥有的rowKey区间[startKey, splitRow), [splitRow, endKey)
    • 将当前状态currentPhase又STARTED转换成PREPARED。

    接下来时SplitTransactionImpl#execute方法的调用,剩下split的所有过程以及涉及到的状态变化都是在这里完成,这里面调用链比较复杂,代码较多,下面是调用链的核心点以及状态变化(大写的表示状态):

    execute()  -->  SplitTransactionImpl# createDaughters 
                          |
                          v
              (PREPARED >> BEFORE_PRE_SPLIT_HOOK)
                          |
                          v
                     RegionCoprocessorHost # preSplit()
                          |
                          v
              (BEFORE_PRE_SPLIT_HOOK >> AFTER_PRE_SPLIT_HOOK)
                          |
                          |-----------> SplitTransactionImpl#stepsBeforePONR()
                                                   |
                                                   v
                   ZkSplitTransactionCordination # startSplitTransaction  
                    [注:startSplitTransaction在zk的/hbase/region-in-transition
                     节点下创建一个parent的encodedName的临时节点, 节点的
                    data包含hri_a, hri_b;
                  以及状态'RS_ZK_REQUEST_REGION_SPLIT'枚举值(10). ]
                                                  |
                                                v
                      (AFTER_PRE_SPLIT_HOOK >> SET_SPLITTING)
                                                 |
                                                v
                ZkSplitTransactionCordination # waitForSplitTransaction
        [注: 上一步创建的节点master被master监听到后将data里的状态修改为'RS_ZK_REGION_SPLITTING'枚举值(5),
            此方法等待master修改完成后返回]
                                                |
                                                v           
                                                                
            this.parent.getRegionFileSystem().createSplitsDir();
                 [注:在hdfs上table下面 parent region目录下面创建
                 一个'.splits'的目录,例如这是笔者defualt.t1表一个region
                  encodedName为a33...的路径的目录:  
                /hbase/data/default/t1/a33bb6a3c49157ab43876540d083e5f1]
                                                  |
                                                   v
                      (SET_SPLITTING >> CREATE_SPLIT_DIR)
                                                   |
                                                   -> parent.close(false)
                                                      |
                                           coprocessorHost#preClose
                                                      |
                                      [注:closing设为true
                                       region不再提供服务,
                                     flush memstore,close all HStore]
                                                      |
                                    coprocessorHost#postClose
                                                  |<--|
                                                  |
                                 (CREATE_SPLIT_DIR >> CLOSED_PARENT_REGION)
                                                  |
                           services.removeFromOnlineRegions(this.parent, null);
                                 [注:service即RegionServerService
                                   此时parent已经是closed状态,不再提供服务,
                                    移除parent。]
                                                  |
                                      ( CLOSED_PARENT_REGION >> OFFLINED_PARENT )
                                                  |
                                                  |--> SplitTransactionImpl # splitStoreFiles
                                                            |
                                                 [注:Region对应table所有了column family,每一个colume family下有HFile,每个HFile都需要split。
                                  下文假设分裂的HFile属于列族cf-n,分裂的HFile文件名了file-n, 被分裂的parent region的encoded name为r-n,b_name, a_name表示分裂后上下两个region的encoded name。
                                      需要split的HFile包装成StoreFileSplitter(实现Callable)提交线程池执行。
                                       对于需要split的HFile,分裂成上下两部分,执行如下:
                                       1.  创建下半部分引用文件(Reference),在'.splits'下创建'a_name/cf-n'路径 ,在cf-n路径下创建名为file-n.r-n的文件。
                                       并在文件中写入bottom, splitKey信息(二进制格式),表示它引用split前hfile的下半部分.
                                       2.  创建上半部引用,在'.splits'下创建'b_name/cf-n'路径 ,在cf-n路径下创建名为file-n.r-n的文件。
                                           并在文件中写入top, splitKey信息(二进制格式),表示它引用split前hfile的上半部分。
                                                  |<--------|
                                                  |
                                                  v
                                              ((OFFLINE_PARENT >> STARTED_REGION_A_CREATION))
                                                  [注: 标记成开始创建hri_a这个region,上一步虽然已完成parent的所有的hfile的split,
    但是,分裂后的region:a,b各自的hfile都在parent_region_encode_name/.splits/a_encoded_name和b_encoded_name下.
    需要移动到正确的目录下,也就是table_name/encoded_region_name/ 这个目录。]
                                                  |
                                           HRegion# parent.createDaughterRegionFromSplits(this.hri_a)
                                                  [注:首先移动region a, region a持有parent region的下部分,步骤如下(假设parent region所属表的目录是/hbase/data/default/table-n):
                                                    1.  创建/hbase/data/default/table-n/a_encoded_name路径   
                                                    2. 在上面路径下创建.regioninfo文件,写入a的RegionInfo二进制信息.
                                                    3. 通过hdfs的rename调用将.splits/a_encoded_name所有文件移动到1中的路径下。至此,region_a创建完,此时a不可用,因为master还没有把新的region调度到某个region server上,metatable也还没有region a的信息。]
                                                  |
                                   ((STARTED_REGION_A_CREATION  >> STARTED_REGION_B_CREATION))
                                                   [注: 省略region b的过程,和a是一样的]
                        |------------------------ |
                        |                                        
                        v
        RegionCoprocessorHost # preSplitBeforePONR
                        |
                        v
         ((STARTED_REGION_B_CREATION  >> PONR))
      [到这一步,region a,b已经的hfile已经写到hdfs中,但是meta table还没更新,依然是parent的,a,b也还没有被指派到RegionServer。
       PONR之后发生任何失败异常,都不会做rollback,而是直接关闭当前region server。
      HMaster检查到region server的失败,会做失败处理,split会继续,meta table会更新,region a,b会被正确管理。]
                        |
                        v
            MetaTableAccessor.splitRegion()
          [注:下线parent,为parent构建一个meta table的Put操作如下:  
            - 将parent的region Info标记为offline=true && split=true(meta中rowkey是regionname,参考3.1 - 1),修改的列为info:regioninfo
            - 增加两列info:splitA和info:splitB,value是a,b的regioninfo 
           在meta table中加入region a, b的信息,为a,b构建Put操作,每一个在meta table中的region至少包含这些信息(列):
               1. info:server,region 所在server。
               2. info:startcode, region所在region server的startcode,是region server启动的时间戳。
               3. info:seqnumDuringOpen, 新region为1 。
               metatable更新成功后,region a,b还没有在region server上打开,所以还不能被访问。]
    |-------------------|
    |
    v                   
    parent.getCoprocessorHost().preSplitAfterPONR();
     [调用注册在parent region上的coprocessor]
    |
    |--------------->stepsAfterPONR()
                [注:并行执行两个线程打开a, b两个region(调用HRegion#openRegion),此时region a,b就可以被访问了。
                通知master完成region的split,即将zk上/hbase/region-in-transition下encoded_parent_name的值改成'RS_ZK_REGION_SPLIT' 枚举值6.]
                            |
       ((PONR  >> BEFORE_POST_SPLIT_HOOK))
                            |
              parent.getCoprocessorHost().postSplit
          [调用parent region注册的coprocessor]
                            |
     ((BEFORE_POST_SPLIT_HOOK  >> AFTER_POST_SPLIT_HOOK))
    |-----------------------|
    |
    v
    ((AFTER_POST_SPLIT_HOOK >> COMPLETED))
    [至此,region server端完成]
                                          
    

    3.3 HMaster端

    这里开始是split过程中HMaster端做的一些事情。回顾3.2 -2 中split过程中哪些需要和HMaster交互:

    1. 调用ZkSplitTransactionCordination # waitForSplitTransaction
      等待HMaster将zk的/hbase/region-in-transition/{encoded-regionname}的data中状态设置由‘ RS_ZK_REQUEST_REGION_SPLIT’改变成‘ RS_ZK_REGION_SPLITTING’。

    2. 调用stepsAfterPONR()过程中,HRegionServer端将/hbase/region-in-transition/{encoded-regionname}的data中状态设置由'RS_ZK_REGION_SPLITTING'变为‘RS_ZK_REGION_SPLIT’,由于HMaster会监控这几zk节点,所以HMaster也会做出处理。

    先说说1, HMaster通过AssignmentManager监控/hbase/region-in-transition节点的改变(可以参考文章HMaster启动), AssignmentManager通过如下的调用:

    nodeCreated -> handleAssignmentEvent -> handleRegion -> handleRegionSplitting 
    

    进入到方法‘handleRegionSplitting’处理RS_ZK_REQUEST_REGION_SPLIT状态,它所在的处理就是将RS_ZK_REQUEST_REGION_SPLIT状态改变成RS_ZK_REGION_SPLITTING使得ZkSplitTransactionCordination # waitForSplitTransaction能够返回继续处理split。

    再是2,同样是在AssignmentManager #handleRegionSplitting中处理,到状态‘ RS_ZK_REGION_SPLIT’说明split已经完成了,这时会处理的是parent以及a,b的replicas。它需要unassign parent的所有replicas,然后建立a,b的replicas,完成replicas region的assign。在方法的最后删除zk /hbase/region-in-transition/{encoded-parent-region-name}这个节点.

    相关文章

      网友评论

        本文标题:HBase学习 - HRegion Split

        本文链接:https://www.haomeiwen.com/subject/nxexkxtx.html