HBase中MemStore flush的源码解析

来源:互联网 发布:软件编程工作工作状态 编辑:程序博客网 时间:2024/05/17 08:48
flush请求的发出
HRegion会调用requestFlush()触发flush行为,flush发生在每一处region可能发生变化的地方,包括region有新数据写入,客户端调用了put/increment/batchMutate等接口。
首先,hbase.hregion.memstore.block.multiplier是个乘数因子,默认值是4,该值会乘上hbase.hregion.memstore.flush.size配置的值(128M),如果当前region上memstore的值大于上述两者的乘积,则该当前region的更新(update)会被阻塞住,对当前region强制发起一个flush。
其次,还有一处要求是整个regionServer上所有memstore的大小之和是否超过了整个堆大小的40%,如果超过了则会阻塞该regionserver上的所有update,并挑选出占比较大的几个region做强制flush,直至降到lower limit以下。
最后,当某个regionserver上的所有WAL文件数达到hbase.regionserver.max.logs(默认是32)时,该regionserver上的memstores会发生一次flush,以减少wal文件的数目,此时flush的目的是控制wal文件的个数,以保证regionserver的宕机恢复时间可控。
flush请求的处理流程: 
    hbase中flush请求的处理流程简化后如下图中所示,图片选自参考链接,这里逐个展开源码中的细节做介绍:

HRegion中requestFlush()的源代码如下所示:
private void requestFlush() {       //通过rsServices请求flush    if (this.rsServices == null) {    //rsServices为HRegionServer提供的服务类      return;    }    synchronized (writestate) {       //检查状态是为了避免重复请求      if (this.writestate.isFlushRequested()) {        return;      }      writestate.flushRequested = true;     //更新writestate的状态    }    // Make request outside of synchronize block; HBASE-818.    this.rsServices.getFlushRequester().requestFlush(this, false);    if (LOG.isDebugEnabled()) {      LOG.debug("Flush requested on " + this);    }}
关键的是下面一句:
this.rsServices.getFlushRequester().requestFlush(this, false);
其中rsServices向RegionServer发起一个RPC请求,getFlushRequester()是RegionServer中的成员变量coreFlusher中定义的方法,该变量是MemStoreFlusher类型,用于管理该RegionServer上的各种flush请求,它里面定义的几个关键变量如下:
private final BlockingQueue<FlushQueueEntry> flushQueue =    new DelayQueue<FlushQueueEntry>();                                              //BlockingQueue阻塞队列 DelayQueue使用优先级队列实现的无界阻塞队列  private final Map<Region, FlushRegionEntry> regionsInQueue =    new HashMap<Region, FlushRegionEntry>();  private AtomicBoolean wakeupPending = new AtomicBoolean();      //原子bool  private final long threadWakeFrequency;  private final HRegionServer server;                             //HRegionServer实例  private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();  private final Object blockSignal = new Object();        //blockSignal定义在这里是作为一个信号量么?????  protected long globalMemStoreLimit;  protected float globalMemStoreLimitLowMarkPercent;  protected long globalMemStoreLimitLowMark;  private long blockingWaitTime;                          //HRegion的一个阻塞更新的等待时间  private final Counter updatesBlockedMsHighWater = new Counter();  private final FlushHandler[] flushHandlers;  private List<FlushRequestListener> flushRequestListeners = new ArrayList<FlushRequestListener>(1);
下面伴随着讲解hbase的flush流程来讲解上述变量的作用。首先看requestFlush(),它将待flush的region放入待处理队列,这里包括了两个队列,flushQueue是一个无界阻塞队列,属于flush的工作队列,而regionsInQueue则用于保存位于flush队列的region的信息。
public void requestFlush(Region r, boolean forceFlushAllStores) {    synchronized (regionsInQueue) {      if (!regionsInQueue.containsKey(r)) {        // This entry has no delay so it will be added at the top of the flush        // queue.  It'll come out near immediately.        FlushRegionEntry fqe = new FlushRegionEntry(r, forceFlushAllStores);        this.regionsInQueue.put(r, fqe);      //将该region上的flush请求放入请求队列        this.flushQueue.add(fqe);      }    }}
至此flush任务已经放入了工作队列,等待flush线程的处理。MemStoreFlusher中的flush工作线程定义在了flushHandler中,初始化代码如下:
int handlerCount = conf.getInt("hbase.hstore.flusher.count", 2);      //用于flush的线程数this.flushHandlers = new FlushHandler[handlerCount];
其中的handlerCount定义了regionserver中用于flush的线程数量,默认值是2,偏小,建议在实际应用中将该值调大一些。
HRegionServer启动的时候,会一并将这些工作线程也启动,start代码如下:
synchronized void start(UncaughtExceptionHandler eh) {    ThreadFactory flusherThreadFactory = Threads.newDaemonThreadFactory(        server.getServerName().toShortString() + "-MemStoreFlusher", eh);    for (int i = 0; i < flushHandlers.length; i++) {      flushHandlers[i] = new FlushHandler("MemStoreFlusher." + i);      flusherThreadFactory.newThread(flushHandlers[i]);      flushHandlers[i].start();    }}
接下来看看这些flusherHandler都做了什么,看看它的run方法吧,里面的主要逻辑列写在下面:
public void run() {      while (!server.isStopped()) {        FlushQueueEntry fqe = null;        try {          wakeupPending.set(false); // allow someone to wake us up again          fqe = flushQueue.poll(threadWakeFrequency, TimeUnit.MILLISECONDS);          if (fqe == null || fqe instanceof WakeupFlushThread) {            if (isAboveLowWaterMark()) {              if (!flushOneForGlobalPressure()) {                                               Thread.sleep(1000);                wakeUpIfBlocking();              }              wakeupFlushThread();      //wakeupFlushThread用作占位符插入到刷写队列中以确保刷写线程不会休眠            }            continue;          }          FlushRegionEntry fre = (FlushRegionEntry) fqe;          if (!flushRegion(fre)) {            break;          }        } catch (InterruptedException ex) {          continue;        } catch (ConcurrentModificationException ex) {          continue;        } catch (Exception ex) {          if (!server.checkFileSystem()) {            break;          }        }      }      synchronized (regionsInQueue) {        regionsInQueue.clear();        flushQueue.clear();      }      // Signal anyone waiting, so they see the close flag      wakeUpIfBlocking();      LOG.info(getName() + " exiting");}
可以看到run方法中定义了一个循环,只要当前regionserver没有停止,则flusherHandler会不停地从请求队列中获取具体的请求fqe,如果当前无flush请求或者获取的flush请求是一个空请求,则根据当前regionServer上全局MemStore的大小判断一下是否需要flush。
这里定义了两个阈值,分别是globalMemStoreLimit和globalMemStoreLimitLowMark,默认配置里前者是整个RegionServer中MemStore总大小的40%,而后者又是前者的95%,为什么要这么设置,简单来说就是,当MemStore的大小占到整个RegionServer总内存大小的40%时,该regionServer上的update操作会被阻塞住,此时MemStore中的内容强制刷盘,这是一个非常影响性能的操作,因此需要在达到前者的95%的时候,就提前启动MemStore的刷盘动作,不同的是此时的刷盘不会阻塞读写。
回到上面的run方法,当需要强制flush的时候,调用的是flushOneForGlobalPressure执行强制flush,为了提高flush的效率,同时减少带来的阻塞时间,flushOneForGlobalPressure中对执行flush的region选择做了很多优化,总体来说,需要满足以下两个条件:
   (1)Region中的StoreFile数量不能过多,意味着挑选flush起来更快的region,减少阻塞时间;
   (2)满足条件1的所有Region中大小为最大值,意味着尽量最大化本次强制flush的执行效果;
ok,如果请求队列中获得了flush请求,那么flush请求具体又是如何处理的呢,从代码中可以看到请求处理在flushRegion方法中,下面分析该方法都做了什么。
它首先会检查当前region内的storeFiles的数量,如果storefile过多,会首先发出一个对该region的compact请求,然后再将region重新加入到flushQueue中等待下一次的flush请求处理,当然,再次加入到flushQueue时,其等待时间被相应缩短了。
this.flushQueue.add(fqe.requeue(this.blockingWaitTime / 100));   //将这次请求的region重新入队
storeFile数量满足要求的flush请求会进入Region的flush实现,除掉日志输出和Metrics记录,主要的代码逻辑记在下面:
private boolean flushRegion(final Region region, final boolean emergencyFlush,      boolean forceFlushAllStores) {    long startTime = 0;    synchronized (this.regionsInQueue) {      FlushRegionEntry fqe = this.regionsInQueue.remove(region);      flushQueue.remove(fqe);    }                                     //将flush请求从请求队列中移除    lock.readLock().lock();               //region加上共享锁    try {      notifyFlushRequest(region, emergencyFlush);      FlushResult flushResult = region.flush(forceFlushAllStores);      boolean shouldCompact = flushResult.isCompactionNeeded();      boolean shouldSplit = ((HRegion)region).checkSplit() != null;      if (shouldSplit) {        this.server.compactSplitThread.requestSplit(region);        //处理flush之后的可能的split      } else if (shouldCompact) {        server.compactSplitThread.requestSystemCompaction(            region, Thread.currentThread().getName());              //处理flush之后的可能compact      }    } catch (DroppedSnapshotException ex) {      server.abort("Replay of WAL required. Forcing server shutdown", ex);      return false;    } catch (IOException ex) {      if (!server.checkFileSystem()) {        return false;      }    } finally {      lock.readLock().unlock();      wakeUpIfBlocking();           //唤醒所有等待的线程    }    return true;}
两点说明,其一是flush期间,该region是被readLock保护起来的,也就是试图获得writeLock的请求会被阻塞掉,包括move region、compact等等;其二是flush之后,可能会产生数量较多的storefile,这会触发一次compact,同样的flush后形成的较大storefile也会触发一次split;
region.flush(forceFlushAllStores)这一句是可看出flush操作是region级别的,也就是触发flush后,该region上的所有MemStore均会参与flush,这里对region又加上了一次readLock,ReentrantReadWriteLock是可重入的,所以倒无大碍。
该方法中还检查了region的状态,如果当前region正处于closing或者closed状态,则不会执行compact或者flush请求,这是由于类似flush这样的操作,一般比较耗时,会增加region的下线关闭时间。
所有检查通过后,开始真正的flush实现,一层层进入调用的函数,最终的实现在internalFlushCache,代码如下:
protected FlushResult internalFlushcache(final WAL wal, final long myseqid,      final Collection<Store> storesToFlush, MonitoredTask status, boolean writeFlushWalMarker)          throws IOException {    PrepareFlushResult result      = internalPrepareFlushCache(wal, myseqid, storesToFlush, status, writeFlushWalMarker);    if (result.result == null) {      return internalFlushCacheAndCommit(wal, status, result, storesToFlush);    } else {      return result.result; // early exit due to failure from prepare stage    }}
其中internalPrepareFlushCache进行flush前的准备工作,包括生成一次MVCC的事务ID,准备flush时所需要的缓存和中间数据结构,以及生成当前MemStore的一个快照。internalFlushCacheAndCommit则执行了具体的flush行为,包括首先将数据写入临时的tmp文件,提交一次更新事务(commit),最后再将文件移入hdfs中的正确目录下。
这里面我找到了几个关键点,其一,该方法是被updatesLock().writeLock()保护起来的,updatesLock与上文中提到的lock一样,都是ReentrantReadWriteLock,这里为什么还要再加锁呢。前面已经加过的锁是对region整体行为而言,如split、move、merge等宏观行为,而这里的updatesLock是数据的更新请求,快照生成期间加入updatesLock是为了保证数据一致性,快照生成后立即释放了updatesLock,保证了用户请求与快照flush到磁盘同时进行,提高系统并发的吞吐量。
其二,那么MemStore的snapshot、flush以及commit操作具体是如何实现的,在internalPrepareFlushCache中有下面的一段代码:
for (Store s : storesToFlush) {       //循环遍历该region的所有storefile,初始化storeFlushCtxs&committedFiles    totalFlushableSizeOfFlushableStores += s.getFlushableSize();    storeFlushCtxs.put(s.getFamily().getName(), s.createFlushContext(flushOpSeqId));    committedFiles.put(s.getFamily().getName(), null); // for writing stores to WAL}
上面这段代码循环遍历region下面的storeFile,为每个storeFile生成了一个StoreFlusherImpl类,生成MemStore的快照就是调用每个StoreFlusherImpl的prepare方法生成每个storeFile的快照,至于internalFlushCacheAndCommit中的flush和commti行为也是调用了region中每个storeFile的flushCache和commit接口。
StoreFlusherImpl中定义的flushCache主要逻辑如下:
protected List<Path> flushCache(final long logCacheFlushId, MemStoreSnapshot snapshot,      MonitoredTask status) throws IOException {    StoreFlusher flusher = storeEngine.getStoreFlusher();    IOException lastException = null;    for (int i = 0; i < flushRetriesNumber; i++) {      try {        List<Path> pathNames = flusher.flushSnapshot(snapshot, logCacheFlushId, status);        Path lastPathName = null;        try {          for (Path pathName : pathNames) {            lastPathName = pathName;            validateStoreFile(pathName);          }          return pathNames;        } catch (Exception e) {          。。。。。        }      } catch (IOException e) {        。。。。。。      }    }    throw lastException;}
其中storeEngine是每个store上的执行引擎,flushSnapshot的目标就是将snapshot写入到一个临时目录,其实现很直观,就是使用一个InternalScanner,一边遍历cell一边写入到临时文件中。最终在commit再将tmp中的文件转移到正式目录,并添加到相应Store的文件管理器中,对用户可见。
以上大概分析了HBase中的flush流程,分析中可能还有不甚准确的地方,欢迎大家一起讨论学习。


参考资料:
http://blog.csdn.net/youmengjiuzhuiba/article/details/45531151

0 0