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
- HBase中MemStore flush的源码解析
- HBase – Memstore Flush深度解析
- HBase Memstore Flush
- HBase源码分析之HRegionServer上MemStore的flush处理流程(一)
- HBase源码分析之HRegionServer上MemStore的flush处理流程(二)
- HBase源码分析之MemStore的flush发起时机、判断条件等详情
- HBase源码分析之MemStore的flush发起时机、判断条件等详情(二)
- HBase源码分析之memstore的实现及flush处理流程
- HBase之MemStore flush流程
- memstore的flush流程分析
- HBase写入性能改造(续)--MemStore、flush、compact参数调优及压缩卡的使用
- HBase写入性能改造(续)--MemStore、flush、compact参数调优及压缩卡的使用
- HBase写入性能改造(续)--MemStore、flush、compact参数调优及压缩卡的使用
- hbase 源代码分析 (13) memStore flush to HFile
- HBase Flush 解析
- Hbase MemStore
- HBase Memstore
- HBase源码分析之HRegion上MemStore的flsuh流程(一)
- ndk
- 10分钟搞懂git和github(该教程只适合快速入门)
- AssetBundle 3 (AssetDatabase.GetAllAssetBundleNames and AssetDatabase.GetAllAssetBundleNames)
- 不借助变量交换a,b值
- Android中view的getHeight()方法的陷阱
- HBase中MemStore flush的源码解析
- 静态映射表的建立过程
- ==和equals的区别
- 【9-1-8】数组与链表的快速排序法
- JavaScript基础知识-数组对象
- 30个php操作redis常用方法代码例子
- HDU-2087 剪花布条 (KMP)
- 多线程 同步集合类
- 向Edit添加内容