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