Hbase源码分析(十二)MemStore的flush处理(下)2021SC@SDUSC


前言

接着上文中提到的问题,本文我们研究HRegionServer上MemStore的flush处理流程,重点讲述下如何选择一个HRegion进行flush以缓解MemStore压力,还有HRegion的flush是怎样发起的。


一.如何选择一个HRegion进行flush以缓解MemStore压力

上文中我们讲到过flush处理线程如果从flushQueue队列中拉取出的一个FlushQueueEntry为为空,或者为WakeupFlushThread,并且通过isAboveLowWaterMark()方法判断全局MemStore的大小高于限制值得低水平线,调用flushOneForGlobalPressure()方法,按照一定策略,flush一个HRegion的MemStore,降低MemStore的大小,预防OOM等异常情况的发生。

flushOneForGlobalPressure()

下面,我们分析下flushOneForGlobalPressure()方法,代码如下:

private boolean flushOneForGlobalPressure() {
	  
	// 获取RegionServer上的在线Region,根据Region的memstoreSize大小倒序排列,得到regionsBySize
    SortedMap<Long, HRegion> regionsBySize =
        server.getCopyOfOnlineRegionsSortedBySize();
 
    // 构造被排除的Region集合excludedRegions
    Set<HRegion> excludedRegions = new HashSet<HRegion>();
 
    boolean flushedOne = false;// 标志位
    while (!flushedOne) {// 循环一次,没有选中的话,再循环,直到选中或者没有可选的Region
      
      // Find the biggest region that doesn't have too many storefiles
      // (might be null!)
      // 选择一个Memstore最大的并且不含太多storefiles的region作为最有可能被选中的region,即bestFlushableRegion
      HRegion bestFlushableRegion = getBiggestMemstoreRegion(
          regionsBySize, excludedRegions, true);
      
      // Find the biggest region, total, even if it might have too many flushes.
      // 选择一个Memstore最大的region,即便是它包含太多storefiles,作为最终可以被选中的备份方案,即bestAnyRegion
      HRegion bestAnyRegion = getBiggestMemstoreRegion(
          regionsBySize, excludedRegions, false);
 
      // 在内存上阈值之上但是没有能够flush的region的话,直接返回false
      if (bestAnyRegion == null) {
        LOG.error("Above memory mark but there are no flushable regions!");
        return false;
      }
 
      HRegion regionToFlush;
      
      // 选择需要flush的region
      // 如果bestAnyRegion的的memstore大小大于bestFlushableRegion的两倍,则选取bestAnyRegion
      if (bestFlushableRegion != null &&
          bestAnyRegion.memstoreSize.get() > 2 * bestFlushableRegion.memstoreSize.get()) {
        // Even if it's not supposed to be flushed, pick a region if it's more than twice
        // as big as the best flushable one - otherwise when we're under pressure we make
        // lots of little flushes and cause lots of compactions, etc, which just makes
        // life worse!
        if (LOG.isDebugEnabled()) {
          LOG.debug("Under global heap pressure: " +
            "Region " + bestAnyRegion.getRegionNameAsString() + " has too many " +
            "store files, but is " +
            StringUtils.humanReadableInt(bestAnyRegion.memstoreSize.get()) +
            " vs best flushable region's " +
            StringUtils.humanReadableInt(bestFlushableRegion.memstoreSize.get()) +
            ". Choosing the bigger.");
        }
        regionToFlush = bestAnyRegion;
      } else {// 否则,优先选取bestFlushableRegion
        if (bestFlushableRegion == null) {
          regionToFlush = bestAnyRegion;
        } else {
          regionToFlush = bestFlushableRegion;
        }
      }
 
      // 检测状态:被选中Region的memstoreSize必须大于0
      Preconditions.checkState(regionToFlush.memstoreSize.get() > 0);
 
      LOG.info("Flush of region " + regionToFlush + " due to global heap pressure");
      
      // 调用flushRegion()方法,针对单个Region,进行MemStore的flush
      flushedOne = flushRegion(regionToFlush, true);
      if (!flushedOne) {// flush失败则添加到excludedRegions集合中,避免下次再被选中
        LOG.info("Excluding unflushable region " + regionToFlush +
          " - trying to find a different region to flush.");
        excludedRegions.add(regionToFlush);
      }
    }
    return true;
  }

这个方法的处理逻辑,如下:

1、获取RegionServer上的在线Region,根据Region的memstoreSize大小倒序排列,得到regionsBySize;

2、构造被排除的Region集合excludedRegions;

3、标志位flushedOne设置为false;

4、循环,直到标志位flushedOne为true,即存在Region被选中,或者根本没有可选的Region:

4.1、循环regionsBySize,选择一个Memstore最大的并且不含太多storefiles的region作为最有可能被选中的region,即bestFlushableRegion:

4.1.1、如果当前region在excludedRegions列表中,直接跳过;

4.1.2、如果当前region的写状态为正在flush,或者当前region的写状态不是写启用,直接跳过;

4.1.3、如果需要检查StoreFile数目,且包含太多StoreFiles,也直接跳过;

4.1.4、否则返回该region;

4.2、循环regionsBySize,选择一个Memstore最大的region,即便是它包含太多storefiles,作为最终可以被选中的备份方案,即bestAnyRegion:

4.2.1、如果当前region在excludedRegions列表中,直接跳过;

4.2.2、如果当前region的写状态为正在flush,或者当前region的写状态不是写启用,直接跳过;

4.2.3、否则返回该region;

4.3、在内存上阈值之上但是没有能够flush的region的话,直接返回false;

4.4、选择需要flush的region:

4.4.1、如果bestAnyRegion的的memstore大小大于bestFlushableRegion的两倍,则选取bestAnyRegion;

4.4.2、否则,优先选取bestFlushableRegion;

4.5、检测状态:被选中Region的memstoreSize必须大于0;

4.6、调用flushRegion()方法,针对单个Region,进行MemStore的flush;

4.7、flush失败则添加到excludedRegions集合中,避免下次再被选中。

以上就是按照一定策略选择一个HRegion进行MemStore的flush以缓解MemStore压力的方法。

二.HRegion的flush是如何发起的

那么,剩下的flush指定HRegion的问题就同接下来我们将要讲的HRegion的flush是如何发起的一致了。

flushRegion()

我们先看下带一个参数的flushRegion()方法,带一个参数的flushRegion()方法,实际上是在拿到一个待flush的HRegion的封装体FlushRegionEntry类型的fqe后,对其做一些必要的判断,决定是直接进行flush还是推后执行,且在第一次推后前,如果需要,则做分裂或系统合并处理。具体处理逻辑如下:
1、如果Region不是MetaRegion且Region上有太多的StoreFiles:

1.1、通过isMaximumWait()判断阻塞时间,已阻塞达到或超过指定时间,记录日志并执行flush,跳到2,结束;

1.2、如果是第一次推迟,记录一条日志信息,然后对该HRegion先请求分裂Split,分裂不成功的话再请求系统合并SystemCompaction;

1.3、再将fqe放回到队列flushQueue,增加延迟时间900ms(参数可配置),等到到期后再从队列中取出来进行处理;

1.4、佯言,该Region被推迟进行flush,结果还不确定,所以应该返回true;

2、调用两个参数的flushRegion()方法,通知HRegion执行flush。

进行阻塞时间的判断:判断当前时间减去创建时间是否大于指定时间。代码如下:

 public boolean isMaximumWait(final long maximumWait) {
      return (EnvironmentEdgeManager.currentTime() - this.createTime) > maximumWait;
    }

然后分析带有两个参数的flushRegion()方法,代码如下:

private boolean flushRegion(final HRegion region, final boolean emergencyFlush) {
    long startTime = 0;
    synchronized (this.regionsInQueue) {
      
      // 先从regionsInQueue中移除对应的HRegion信息
      FlushRegionEntry fqe = this.regionsInQueue.remove(region);
      // Use the start time of the FlushRegionEntry if available
      if (fqe != null) {
        // 获取flush的开始时间startTime
    	startTime = fqe.createTime;
      }
      if (fqe != null && emergencyFlush) {
        
        flushQueue.remove(fqe);
     }
    }
    
    // 获取flush的开始时间startTime
    if (startTime == 0) {
     
      startTime = EnvironmentEdgeManager.currentTime();
    }
    
    // 上读锁,意味着与其他拥有读锁的线程不冲突,可以同步进行,而与拥有写锁的线程互斥
    lock.readLock().lock();
    try {
      
      // 通过监听器Listener通知flush请求者flush的type
      notifyFlushRequest(region, emergencyFlush);
      
      // 调用HRegion的flushcache()方法,执行MemStore的flush
      HRegion.FlushResult flushResult = region.flushcache();
      
      // 根据flush的结果,判断下一步该做如何处理
      
      // 判断是否应该进行合并compact
      boolean shouldCompact = flushResult.isCompactionNeeded();
      // We just want to check the size
      
      // 检测是否应该进行分裂split
      boolean shouldSplit = region.checkSplit() != null;
      
      // 必要的情况下,先进行split,再进行system compact
      if (shouldSplit) {
        this.server.compactSplitThread.requestSplit(region);
      } else if (shouldCompact) {
        server.compactSplitThread.requestSystemCompaction(
            region, Thread.currentThread().getName());
      }
      
      // 如果flush成功,获取flush结束时间,计算耗时,记录HRegion上的度量信息
      if (flushResult.isFlushSucceeded()) {
        long endTime = EnvironmentEdgeManager.currentTime();
        server.metricsRegionServer.updateFlushTime(endTime - startTime);
      }
    } catch (DroppedSnapshotException ex) {
    
      server.abort("Replay of WAL required. Forcing server shutdown", ex);
      return false;
    } catch (IOException ex) {
      LOG.error("Cache flush failed" +
        (region != null ? (" for region " + Bytes.toStringBinary(region.getRegionName())) : ""),
        RemoteExceptionHandler.checkIOException(ex));
      if (!server.checkFileSystem()) {
        return false;
      }
    } finally {
      // 释放读锁
      lock.readLock().unlock();
      
      // 唤醒阻塞的其他线程
      wakeUpIfBlocking();
    }
    return true;
  }

带有两个参数的flushRegion()方法大体逻辑如下:
1、首选处理regionsInQueue集合和flushQueue队列:

1.1、先从regionsInQueue中移除对应的HRegion信息,这个无论是否紧急flush,都是必须要做的;

1.2、获取flush的开始时间startTime;

1.3、如果是紧急刷新,需要从flushQueue队列中移除对应的fqe,如果不是紧急刷新,fqe将通过flushQueue.poll被移除;

2、如果startTime为null,获取flush的开始时间startTime;

3、上读锁,意味着与其他拥有读锁的线程不冲突,可以同步进行,而与拥有写锁的线程互斥(后期将会写专门的文章分析HBase内部各流程中锁的应用);

4、通过监听器Listener通知flush请求者flush的type;

5、调用HRegion的flushcache()方法,执行MemStore的flush,并获得flush结果;

6、根据flush的结果,判断下一步该做如何处理:

6.1、根据flush结果判断是否应该进行合并compact,即标志位shouldCompact;

6.2、调用HRegion的checkSplit()方法检测是否应该进行分裂split,即标志位shouldSplit;

6.3、通过两个标志位判断,必要的情况下,先进行split,再进行system compact;

7、如果flush成功,获取flush结束时间,计算耗时,记录HRegion上的度量信息;

8、最后释放读锁,唤醒阻塞的其他线程。

通过监听器Listener通知flush请求者flush的type如下:

private void notifyFlushRequest(HRegion region, boolean emergencyFlush) {
    
	// 默认类型为 FlushType.NORMAL
	FlushType type = FlushType.NORMAL;
    
	// 如果是紧急刷新,跟是否在高水位线上来确定type,高水位线上为FlushType.ABOVE_HIGHER_MARK,低水位线上为FlushType.ABOVE_LOWER_MARK
	if (emergencyFlush) {
      type = isAboveHighWaterMark() ? FlushType.ABOVE_HIGHER_MARK : FlushType.ABOVE_LOWER_MARK;
    }
	
	// 针对监听器逐个添加region、type
    for (FlushRequestListener listener : flushRequestListeners) {
      listener.flushRequested(type, region);
    }
  }

flush结果FlushResult,它是HRegion中的一个静态内部类,包括一个Result枚举,其中包含的flush结果如下:
1、FLUSHED_NO_COMPACTION_NEEDED:flush成功,但是不需要执行compact;

2、FLUSHED_COMPACTION_NEEDED:flush成功,同时需要执行compact;

3、CANNOT_FLUSH_MEMSTORE_EMPTY:无法进行flush,因为MemStore为空;

4、CANNOT_FLUSH:无法进行flush。

判断flush是否成功,则就是看result是否为FLUSHED_NO_COMPACTION_NEEDED或FLUSHED_COMPACTION_NEEDED,判断是否需要进行compact,则就是看result是否为FLUSHED_COMPACTION_NEEDED。

总结

以上就是今天要讲的内容,到现在HRegionServer上MemStore的flush处理流程全部分析完了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值