HBase Split流程源码分析

来源:互联网 发布:网络球机接线图 编辑:程序博客网 时间:2024/06/06 11:04

分析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();           //唤醒所有等待的线程    }
这里多说一句,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删除掉。



0 0
原创粉丝点击