HBase Split流程源码分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/bryce123phy/article/details/52152578

分析hbase的split流程,同样地我们先从regionserver中的相应线程作为突破口,依次分析split的触发条件和split的执行实现。

split的触发条件

我们在compact流程分析中讲解过,hbase的regionserver中维护着一个CompactSplitThread类型的变量,所有的compact/split请求都会交给该变量处理,前面我们分析了compact在CompactSplitThread中的处理,同样的,我们接下来将会分析split是如何在CompactSplitThread中被处理的。首先还是看CompactSplitThread中定义的几个与split相关的变量。

private final ThreadPoolExecutor longCompactions;     //long合并线程池
private final ThreadPoolExecutor shortCompactions;    //short合并线程池
private final ThreadPoolExecutor splits;              //split线程池
private final ThreadPoolExecutor mergePool;           //merge线程池
private int regionSplitLimit; 
需要注意的是最后一个整型变量regionSplitLimit,它定义了一个regionserver所持有的最大region总数,如果region的数量超过了它的限制,则split不会再发生。该变量的值由hbase.regionserver.regionSplitLimit配置,默认是1000;splits是该regionserver用于参与split的线程池,线程池中的线程数量由“hbase.regionserver.thread.split”配置,默认是1。

region发生split时,调用方会调用CompactSplitThread中的requestSplit方法,该方案将split请求包装成一个Runnable的SplitRequest对象放入前文所说的线程池split中去执行。

this.splits.execute(new SplitRequest(r, midKey, this.server));
分析requestSplit方法的调用时机就可以理出region发生split行为的时间。我们直接列出结果如下:

第一种方式是region发生compact之后,此时会形成比较大的文件,split会在这个时候被调用;

第二种方式是region发生flush的时候,这一部分的代码如下:

    lock.readLock().lock();               
    try {
      notifyFlushRequest(region, emergencyFlush);
      FlushResult flushResult = region.flush(forceFlushAllStores);
      boolean shouldCompact = flushResult.isCompactionNeeded();
      // We just want to check the size
      boolean shouldSplit = ((HRegion)region).checkSplit() != null;
      if (shouldSplit) {
        this.server.compactSplitThread.requestSplit(region);        
      } else if (shouldCompact) {
        server.compactSplitThread.requestSystemCompaction(
            region, Thread.currentThread().getName());             
      }
      if (flushResult.isFlushSucceeded()) {
        long endTime = EnvironmentEdgeManager.currentTime();
        server.metricsRegionServer.updateFlushTime(endTime - startTime);      //将本次flush的时间记录下来
      }
    } catch (DroppedSnapshotException ex) {
      ..........
    } catch (IOException ex) {
      ..........
    } finally {
      lock.readLock().unlock();
      wakeUpIfBlocking();           //唤醒所有等待的线程
    }

可以看到这里调用checkSplit并检查其返回的结果是否是null来确认是否可split,checkSplit调用splitPolicy做判断,在1.X.Xhbase中的默认split策略实现是IncreasingToUpperBoundingRegionSplitPolicy。这种策略取region所属的table在该region所在的regionserver上的region总数记为tableRegionsCount,采用以下公式计算阈值:tableRegionsCount*tableRegionsCount*tableRegionsCount*Memstoreflushsize*2,如果region中某个store的大小大于上述值,则标记该region需要被split。紧接着就是获取split的切分点,split point一般被指定为最大store上最大storefile的中心点。

这里多说一句,region上的memstore发生flush的时候会获取readLock。readLock是读锁,读写锁是一种多线程同步的方案,所谓读锁其实就是共享锁,所谓写锁就是排他锁,当一个线程获取读锁时,所有其他以读模式访问的线程都可以获得访问权,而已写模式对它进行加锁的线程都会被阻塞。而当一个线程获取写锁的时候,所有其他试图获取锁的线程都会被阻塞。这里获取了readLock,所以flush时对region的更新都会被阻塞。

第三种调用是在RSRpcServices的splitRegion函数中,表示的是用户对region发出的split请求。

split的执行实现

接上文,摸清了split的请求发起时机之后,接下来分析split是如何实现的,前面说过spilt请求会包装成SplitRequest类型的对象交由splits线程处理。所以具体的split实现是在SplitRequest的run方法中,下面我们筛选出run方法的重点流程以分析split的实现:

public void run() {
    .......
    SplitTransactionImpl st = new SplitTransactionImpl(parent, midKey);
    try {
       tableLock = server.getTableLockManager().readLock(.........);
       try {
        tableLock.acquire();
       } catch (IOException ex) {
          .......
       }
       if (!st.prepare()) return;
       try {
          st.execute(this.server, this.server);
       } catch (Exception e) {
          .......
       }
    } catch (Exception e) {
       server.checkFileSystem();
    } finally {
       //处理post coprocessor
       releaseTableLock();
       //更新metrics
    }
}
ok,除了一些异常处理和回滚,run方法的主要逻辑已经梳理出来了,可以看出split前首先获取了table的共享锁,这样做的目的是防止其它并发的table修改行为修改当前table的状态或者schema等。然后初始化一个SplitTransactionImpl类型的变量,依次调用它的prepare和execute方法完成region切割。
perpare用以完成split的前期准备,包括构造两个子HRegionInfo,分别是hri_a和hri_b,其中hri_a的startKey胃parent的startKey,endKey为midKey;hri_b的startKey为midKey,endKey为parent的endKey。

接下来在execute方法中,主要代码包括以下三步:

PairOfSameType<Region> regions = createDaughters(server, services);
if (this.parent.getCoprocessorHost() != null) {
   this.parent.getCoprocessorHost().preSplitAfterPONR();
}
regions = stepsAfterPONR(server, services, regions);
transition(SplitTransactionPhase.COMPLETED);
从createDaughters方法进去,我们一点点分析split的实现,其中比较关键的步骤是createDaughters,我们把该方法的主要逻辑列出在下面:

PairOfSameType<Region> daughterRegions = stepsBeforePONR(server, services, testing);
List<Mutation> metaEntries = new ArrayList<>;
if (!testing && useZKForAssignment) {
    if (metaEntries == null || metaEntries.isEmpty()) {
       MetaTableAccessor.splitRegion(server.getConnection(),
         parent.getRegionInfo(), daughterRegions.getFirst().getRegionInfo(),
         daughterRegions.getSecond().getRegionInfo(), server.getServerName(),
         parent.getTableDesc().getRegionReplication());
    } else {
       offlineParentInMetaAndputMetaEntries(server.getConnection(),
         parent.getRegionInfo(), daughterRegions.getFirst().getRegionInfo(), daughterRegions
              .getSecond().getRegionInfo(), server.getServerName(), metaEntries,
              parent.getTableDesc().getRegionReplication());
    }
} else if (services != null && !useZKForAssignment) {
    ..........
}

createDaughters返回的regions实际上是由stepsBeforePONR返回的,下面我们列出stepsBeforePONR中的主要逻辑:

this.parent.getRegionFileSystem().createSplitsDir();
try {
   hstoreFilesToSplit = this.parent.close(false);
} catch (Exception e) {
   .........
}
Pari<Integer,Integer> expectedReferences = splitStoreFiles(hstoreFilesToSplit);
Region a = this.parent.createDaughterRegionFromSplits(this.hri_a);
Region b = this.parent.createDaughterRegionFromSplits(this.hri_b);
return new PairOfSametype<Region>(a,b);
上面我们列出了几个主要步骤,穿插在这些主要步骤之间的是将当前split的状态更新到zookeeper的节点上,hbase的region在发生split的同时,会在zookeeper的region-in-transition目录下创建一个节点,供master同步监听新region的上线和老region的下线这些信息,此外如果split中间发生错误,也需要zookeeper上的状态信息同步以协调region之间的变化。

首先createSplitsDir在parent region的目录下创建了一个.split目录,接着在.split目录下创建daughter region A和region B两个子目录,然后调用close将parent关掉,调用是传入的参数false表示在关掉parent region前先强制执行一次flush,将region memstore中的数据刷写到磁盘。关闭region后region server将region标记为offline状态。

region的关闭在实现上是提交了该region拥有的store到一个线程池中,然后每个store的close方法进行关闭的,store的关闭结果异步获取。hbase在实现这一步中使用了CompletionService,返回结果通过CompletionService的take方法获取,使用这种方式的优势就是当多线程启动了多个Callable之后,每个callable都会返回一个future,CompletionService自己维护了一个线程安全的list,保证先完成的future一定先返回。

现在回来继续讲解我们的split流程,进入splitStoreFiles方法,该方法实际上是为parent中的每个storeFile创建了两个索引文件,核心代码在下面的片段中:

ThreadFactoryBuilder builder = new ThreadFactoryBuilder();
builder.setNameFormat("StoreFileSplitter-%1$d");
ThreadFactory factory = builder.build();
ThreadPoolExecutor threadPool =
   (ThreadPoolExecutor) Executors.newFixedThreadPool(maxThreads, factory);
List<Future<Pair<Path,Path>>> futures = new ArrayList<Future<Pair<Path,Path>>> (nbFiles);

// Split each store file.
for (Map.Entry<byte[], List<StoreFile>> entry: hstoreFilesToSplit.entrySet()) {
   for (StoreFile sf: entry.getValue()) {
      StoreFileSplitter sfs = new StoreFileSplitter(entry.getKey(), sf);
      futures.add(threadPool.submit(sfs));         //storefile被提交执行split了
   }
}
StoreFileSpliitter最终调用HRegionFileSystem中的下面一句代码完成索引文件的创建:

Reference r =
      top ? Reference.createTopReference(splitRow): Reference.createBottomReference(splitRow);
回到stepsBeforePONR方法,最后两步根据子region的元信息创建了HRegion A和B,实际上就是创建了A/B的实际存储目录。完成这些后stepsBeforePONR方法返回,然后调用如下的代码修改meta表中parent region和分裂出的region A和region B的信息。
MetaTableAccessor.splitRegion
splitRegion是一个原子方法,它将父region的信息置为offline,并写入子region的信息,但是此时的子region A和B还不能对外提供服务。需要等待regionServer打开该子region才可以,带着这个疑问,我们进入split流程的最后一个方法——stepsAfterPORN,在该方法中调用openDaughters将子RegionA和RegionB打开以接受写请求,regionsrver打开A和B之后会补充上述两个子region在.META.表中的信息,此时客户端才能够发现两个子region并向该两个region发送请求。

负责open region的线程是继承了HasThread接口的DaughterOpener类,主要包括了下面两个步骤:

1> 调用openHRegion函数的initilize,主要步骤如下:

    a、向hdfs上写入.regionInfo文件以便meta挂掉时恢复;

    b、初始化其下的HStore,主要是调LoadStoreFiles函数;

2> 将子Region添加到rs的online region列表上,并添加到meta表中;
最后更新zk节点上/hbase/region-in-transition/region-name节点的状态为COMPLETED,指示split结束。

Split过程结束后,HDFS和META中还会保留着指向parent region索引文件的信息,这些索引文件会在子region执行major compact重写的时候被删除掉。master的Garbage Collection任务会周期性地检查子region中是否还包含指向parents Region的索引文件,如果不再包含,此时master会将parent Region删除掉。



猜你喜欢

转载自blog.csdn.net/bryce123phy/article/details/52152578