HBase Flush 解析

在对hbase操作中,数据读取/写入都是发生在某个HRegion下某个Store里的files。那么究竟在写入hbase时,一个region下到底发生了什么呢?
常见的有以下三种情况:

1)、memstore flush to disk

2)、columnfamily’s files compaction

3)、region split

首先介绍一些概念:hbase一个表(table)会分割为n个region(在建表时可以指定多少个以及每个表的key range,同时也会在运行时split),这些region会均匀分布在集群的regionserver上。一个region(HRegion)下会有一定数量的column family(一个cf称为一个Store,包含一个MemStore),hbase是按列存储,所以column family是其hdfs对应的最细粒度的文件夹,文件夹的名字即是cf的名字,里面躺着一定数量的hfile(称为StoreFile)。如下图所示:

这里写图片描述

Memstore是HBase框架中非常重要的组成部分之一,是HBase能够实现高性能随机读写至关重要的一环。深入理解Memstore的工作原理、运行机制以及相关配置,对hbase集群管理、性能调优都有着非常重要的帮助。

Memstore 概述

HBase中,Region是集群节点上最小的数据服务单元,用户数据表由一个或多个Region组成。在Region中每个ColumnFamily的数据组成一个Store。每个Store由一个Memstore和多个HFile组成,如下图所示:

这里写图片描述

HBase是基于LSM-Tree模型的,所有的数据更新插入操作都首先写入Memstore中(同时会顺序写到日志HLog中),达到指定大小之后再将这些修改操作批量写入磁盘,生成一个新的HFile文件,这种设计可以极大地提升HBase的写入性能;另外,HBase为了方便按照RowKey进行检索,要求HFile中数据都按照RowKey进行排序,Memstore数据在flush为HFile之前会进行一次排序,将数据有序化;还有,根据局部性原理,新写入的数据会更大概率被读取,因此HBase在读取数据的时候首先检查请求的数据是否在Memstore,写缓存未命中的话再到读缓存中查找,读缓存还未命中才会到HFile文件中查找,最终返回merged的一个结果给用户。

可见,Memstore无论是对HBase的写入性能还是读取性能都至关重要。其中flush操作又是Memstore最核心的操作,接下来重点针对Memstore的flush操作进行深入地解析:首先分析HBase在哪些场景下会触发flush,然后结合源代码分析整个flush的操作流程,最后再重点整理总结和flush相关的配置参数,这些参数对于性能调优、问题定位都非常重要。

Memstore Flush触发方式:

1) Manual调用,HRegionInterface.flushRegion,可以被用户态org.apache.Hadoop.hbase.client.HBaseAdmin调用flush操作实现,该操作会直接触发HRegion的internalFlush。

2)HRegionServer的一次更新操作,使得整个内存使用超过警戒线。警戒线是globalMemStoreLimit, RS_JVM_HEAPSIZE * conf.getFloat(“hbase.regionserver.global.memstore.upperLimit”),凡是超过这个值的情况,会直接触发FlushThread,从全局的HRegion中选择一个,将其MemStore刷入hdfs,从而保证rs全局的memstore容量在可控的范围。

HBase会在如下几种情况下触发flush操作, 需要注意的是MemStore的最小flush单元是HRegion而不是单个MemStore。可想而知,如果一个HRegion中Memstore过多,每次flush的开销必然会很大,因此我们也建议在进行表设计的时候尽量减少ColumnFamily的个数。

Memstore级别限制:当Region中任意一个MemStore的大小达到了上限(hbase.hregion.memstore.flush.size,默认128MB),会触发Memstore刷新。
Region级别限制:当Region中所有Memstore的大小总和达到了上限(hbase.hregion.memstore.block.multiplier * hbase.hregion.memstore.flush.size,默认 2* 128M = 256M),会触发memstore刷新。
Region Server级别限制:当一个Region Server中所有Memstore的大小总和达到了上限(hbase.regionserver.global.memstore.upperLimit * hbase_heapsize,默认 40%的JVM内存使用量),会触发部分Memstore刷新。Flush顺序是按照Memstore由大到小执行,先Flush Memstore最大的Region,再执行次大的,直至总体Memstore内存使用量低于阈值(hbase.regionserver.global.memstore.lowerLimit * hbase_heapsize,默认 38%的JVM内存使用量)。
当一个Region Server中HLog数量达到上限(可通过参数hbase.regionserver.max.logs配置)时,系统会选取最早的一个 HLog对应的一个或多个Region进行flush
HBase定期刷新Memstore:默认周期为1小时,确保Memstore不会长时间没有持久化。为避免所有的MemStore在同一时间都进行flush导致的问题,定期的flush操作有20000左右的随机延时。
手动执行flush:用户可以通过shell命令 flush ‘tablename’或者flush ‘region name’分别对一个表或者一个Region进行flush。

Memstore Flush流程

为了减少flush过程对读写的影响,HBase采用了类似于两阶段提交的方式,将整个flush过程分为三个阶段:

prepare阶段:遍历当前Region中的所有Memstore,将Memstore中当前数据集kvset做一个快照snapshot,然后再新建一个新的kvset。后期的所有写入操作都会写入新的kvset中,而整个flush阶段读操作会首先分别遍历kvset和snapshot,如果查找不到再会到HFile中查找。prepare阶段需要加一把updateLock对写请求阻塞,结束之后会释放该锁。因为此阶段没有任何费时操作,因此持锁时间很短。
flush阶段:遍历所有Memstore,将prepare阶段生成的snapshot持久化为临时文件,临时文件会统一放到目录.tmp下。这个过程因为涉及到磁盘IO操作,因此相对比较耗时。
commit阶段:遍历所有的Memstore,将flush阶段生成的临时文件移到指定的ColumnFamily目录下,针对HFile生成对应的storefile和Reader,把storefile添加到HStore的storefiles列表中,最后再清空prepare阶段生成的snapshot。

上述flush流程可以通过日志信息查看:

/******* prepare阶段 ********/
2016-02-04 03:32:41,516 INFO  [MemStoreFlusher.1] regionserver.HRegion: Started memstore flush for sentry_sgroup1_data,{\xD4\x00\x00\x01|\x00\x00\x03\x82\x00\x00\x00?\x06\xDA`\x13\xCAE\xD3C\xA3:_1\xD6\x99:\x88\x7F\xAA_\xD6[L\xF0\x92\xA6\xFB^\xC7\xA4\xC7\xD7\x8Fv\xCAT\xD2\xAF,1452217805884.572ddf0e8cf0b11aee2273a95bd07879., current region memstore size 128.9 M

/******* flush阶段 ********/
2016-02-04 03:32:42,423 INFO  [MemStoreFlusher.1] regionserver.DefaultStoreFlusher: Flushed, sequenceid=1726212642, memsize=128.9 M, hasBloomFilter=true, into tmp file hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/.tmp/021a430940244993a9450dccdfdcb91d

/******* commit阶段 ********/
2016-02-04 03:32:42,464 INFO  [MemStoreFlusher.1] regionserver.HStore: Added hdfs://hbase1/hbase/data/default/sentry_sgroup1_data/572ddf0e8cf0b11aee2273a95bd07879/d/021a430940244993a9450dccdfdcb91d, entries=643656, sequenceid=1726212642, filesize=7.1 M

RS上HRegion的选择算法:

步骤1:RS上的Region,按照其MemStore的容量进行排序。

步骤2:选出Region下的Store中的StoreFile的个数未达到hbase.hstore.blockingStoreFiles,并且MemStore使用最多的Region。— bestFlushableRegion

步骤3:选出Region下的MemStore使用最多的Region。— bestAnyRegion

步骤4:如果bestAnyRegion的memstore使用量超出了bestFlushableRegion的两倍,这从另外一个角度说明,虽然当前bestAnyRegion有超过blockingStoreFiles个数的文件,但是考虑到RS内存的压力,冒着被执行Compaction的风险,也选择这个Region,因为收益大。否则,直接使用bestFlushableRegion。

指定的Region写入hdfs的过程:

步骤1:获得updatesLock的写锁,阻塞所有对于该Region的更新操作。由此,可知Flush操作会阻塞Region区域内Row的更新操作(Put、Delete、Increment),因为在阻塞更新操作期间,涉及到Memstore的snapshot操作,如果不做限制,那么很可能一个put操作的多个KV,分别落在kvset和snapshot当中,从而与hbase保证row的原子性相悖。

步骤2:mvcc推进一次写操作事务。每个Region维护了一个mvcc对象(Multi Version

Consistency Control),用来控制读写操作的事务性。

步骤3:从HLog中获取一个新的newSeqNum,更新HLog的lastSeqWritten。由于此时该Region的更新操作会暂停,因此,会暂时删除lastSeqWritten记录的RegionName,lastSeqNum,写入”snp”+RegionName, newSeqNum到lastSeqWritten中。这里的lastSeqWritten是HLog用来存储每个Regiond到当前时刻最后一次提交操作的SeqNum。

步骤4:为Region下的每个Store的MemStore执行snapshot操作。

这里写图片描述

如上图所示,HRegion上Store的个数是由Table中ColumnFamily的个数确定,每个Store是由一个MemStore和数个StoreFile(HFile)文件组成,在正常的更新操作过程中,更新的内容会写入MemStore里的kvset结构中。HRegion执行Flush操作,实际上是把MemStore的内容全部刷入hdfs的过程。虽然,目前更新操作已经通过加写锁阻塞,可是读操作仍然可以继续,因此,在memstore执行snapshot的过程中,通过reference,snapshot会指向kvset,然后给kvset指向一个全新的内存区域。代码如下:

这里写图片描述

步骤5:释放updatesLock的写锁,此时该HRegion可以接收更新操作。

步骤6:更新mvcc读版本到当前写版本号。

这里有一个小插曲,在更新操作时,mvcc. completeMemstoreInsert 的操作在updatesLock的范围之外,这样在多线程高并发情况下,就存在已经写入MemStore的kvset当中,但是事务还未完成提交的情况。该场景相关代码如下:

这里写图片描述

我们可以清晰看到,通过updatesLock保证了更新操作写入了MemStore的kvset,但假定Flush线程在其它更新线之后,获得了updatesLock写锁,并执行了snapshot操作。那么,这里的mvcc就会出现读写的事务号不一致的情况,因此,在Region的Flush线程就需要使用waitForRead(w),等待更新到目前写版本号。

步骤7:将Store内的snapshot写成一个StoreFile临时文件。

步骤8:重命名storefile文件,更新Store里文件和Memstore状态。

在步骤8完成之前,整个Hregion的读请求,是和之前没有影响的。因为在读请求过程中,StoreScanner对于kvset和snapshot进行进行同步读取,即使kvset切换成snapshot,scan的操作仍然可以继续,这部分的内容是由MemStoreScanner来控制。

在读过程中,Store里的scanner有两部份,一个是StoreFileScanner,另外一个是MemStoreScanner,它们都继承了KeyValueScanner接口,并通过StoreScanner中的KeyValueHeap封装起来。于此类似,在RegionScannerImpl也是通过一个KeyValueHeap把每个Store的StoreScanner封装起来,从而直接提供对外的服务。

读到这里,可能细心的工程师们,就会有一个疑问:Flush操作对于读的影响究竟有没有呢?

有影响,但比较小。在步骤8以前那些阶段,MemStoreScanner做到了对于kvset与snapshot的自由切换。

这里写图片描述

如上所示,如果kvset被重置,那么theNext将不再等于kvsetNextRow,从而切换成开始从snapshot迭代器中获取数据。

因此,在步骤1~7之间,对于读服务影响不大。但是在步骤8操作最后一步时,需要把生成storefile更新到可用的Store中的StoreFile列表,并清除snapshot的内容。

于是,此时ChangedReaderOberver就开始起作用了。

// Tell listeners of the change in readers.

notifyChangedReadersObservers();

这里写图片描述

这里最为关键的是,将storescanner用来封装全部StoreFileScanner和MemStoreScanner的heap清空,它会触犯的作用是在执行next()操作时,会触发resetScannerStack操作,会重新加载Store下的所有Scanner,并执行seek到最后一次更新的key。这个过程会使得flush操作对于某些next操作变得突然顿一下。

MemStore flush的源码解析

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的文件管理器中,对用户可见。

  • 10
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值