天天看点

HBase源码分析之HRegion上MemStore的flsuh流程(二)

        继上篇《HBase源码分析之HRegion上MemStore的flsuh流程(一)》之后,我们继续分析下HRegion上MemStore flush的核心方法internalFlushcache(),它的主要流程如图所示:

HBase源码分析之HRegion上MemStore的flsuh流程(二)

        其中,internalFlushcache()方法的代码如下:

        又是一个大方法。莫慌,我们慢慢来分析:

        1、首先,需要判断下HRegion上的RegionServer相关的服务是否正常;

        2、获取开始时间,方便记录耗时,以体现系统的性能;

        3、如果没有可以刷新的缓存,直接返回,但是我们需要安全的更新Region的sequence id;

        4、设置状态跟踪器的状态:获取锁以阻塞并发的更新,即Obtaining lock to block concurrent updates;

        5、获得updatesLock的写锁,阻塞所有对于该Region上数据的更新操作,注意,这里用的是updatesLock,而不是lock;

        6、设置状态跟踪器的状态:正在准备通过创建存储的快照刷新,即Preparing to flush by snapshotting stores in...;

        7、创建两个缓存容器:storeFlushCtxs列表和committedFiles映射集合,用来存储刷新过程中的刷新上下文和已完成文件路径;

        8、创建刷新的序列号ID,即flushSeqId,初始化为-1;

        9、mvcc推进一次写操作事务,此时w中的写序号为0,获得多版本一致性控制器中的写条目;

        10、获取刷新序列号ID,如果wal不为空,通过wal取下一个序列号,否则设置为-1:

                10.1、调用wal的startCacheFlush()方法,在HRegion上开启一个flush操作:

                           10.1.1、调用closeBarrier.beginOp()方法,确定开始一个flush操作;

                           10.1.2、Region名对应的最近序列化Id从数据结构oldestUnflushedRegionSequenceIds移动到lowestFlushingRegionSequenceIds中;

                10.2、 wal不为空的话,获取下一个序列号,赋值给flushSeqId;

        11、循环该Region所有的store,预处理storeFlushCtxs、committedFiles:

                11.1、累加每个store可以flush的memstore大小至totalFlushableSize;

                11.2、将每个store对应的StoreFlushContext添加到ArrayList列表storeFlushCtxs中,实际生成的是StoreFlusherImpl实例,该对象只有cacheFlushSeqNum一个变量被初始化为flushSeqId;

                11.3、初始化committedFiles:将每个store对应的列名放置到committedFiles的key中,value暂时为null;

        12、在WAL中写一个刷新的开始标记,并获取一个事务ID--trxId,其实就是往WAL中append一条记录:row为Region所在的startKey,family为METAFAMILY,qualifier为HBASE::FLUSH,value为FlushDescriptor;

        13、循环storeFlushCtxs,为每个StoreFlushContext做准备工作,主要是生成memstore的快照,刷新前的准备工作如下:

                13.1、获取memstore的快照,并赋值到snapshot;

                13.2、获取flush的数目,即待刷新cell数目,并赋值到cacheFlushCount;

                13.3、获取flush的大小,并赋值到cacheFlushSize;

                13.4、创建空的已提交文件列表,大小为1;

        14、快照创建好后,释放写锁updatesLock;

        15、设置状态跟踪器的状态:完成了memstore的snapshot创建;

        16、真正flush之前,先设置一个多版本一致性控制器的写序号,值为本次flush的序列号;

        17、然后,调用多版本控制器的方法,等待其他的事务完成;

        18、设置w为null,防止mvcc.advanceMemstore在finally模块再次被调用;

        19、设置状态跟踪器的状态:刷新stores进行中...;

        20、失败的情况下,标记当前w为已完成;

        21、循环storeFlushCtxs,对每个StoreFlushContext执行刷新操作flushCache,将数据真正写入文件:

                 21.1、调用HStore对象的flushCache()方法,将数据真正写入文件;

        22、循环storeFlushCtxs,对每个StoreFlushContext执行commit操作;

        23、设置flush之后的memstore的大小,减去totalFlushableSize;

        24、将flush标记写入WAL,同时执行sync;

        25、调用WAL的completeCacheFlush()方法完成MemStore的flush:将Region对应的最近一次序列化ID从数据结构lowestFlushingRegionSequenceIds中删除,并调用closeBarrier.endOp()终止一个操作;

        26、记录当前时间为上次flush时间;

        27、将本次flush序列号ID赋值给lastFlushSeqId;

        28、最后唤醒等待memstore的线程;

        29、设置状态追踪状态:完成;

        30、返回flush结果。

        我的天哪!在没有考虑异常的情况下,居然有整整30个步骤!这样一看,显得很啰嗦、麻烦,我们不如化繁为简,把握主体流程。实际上,整个flush的核心流程不外乎以下几大步骤:

        第一步,上锁,标记状态,而且是上了两把锁:外层是控制HRegion整体行为的锁lock,内层是控制HRegion读写的锁updatesLock;

        第二步,获取flush的序列化ID,并通过多版本一致性控制器mvcc推进一次写事务;

        第三步,通过closeBarrier.beginOp()在HRegion上开启一个操作,避免其他操作(比如compact、split等)同时执行;

        第四步,在WAL中写一个flush的开始标记,并获取一个事务ID;         

        第五步,生成memstore的快照;

        第六步,快照创建好后,释放第一把锁updatesLock,此时客户端又可以发起读写请求;

        第七步,利用多版本一致性控制器mvcc等待其他事务完成;

        第八步,将数据真正写入文件,并提交;

        第九步,在WAL中写一个flush的结束标记;

        第十步,通过调用closeBarrier.endOp()在HRegion上终止一个操作,允许其他操作继续执行。

        这样的话,我们看着就比较顺,比较简单了。不得不说,整个flush设计的还是比较严谨和巧妙地。为什么这么说呢?

        首先,严谨之处体现在,宏观上,它利用closeBarrier.beginOp()和closeBarrier.endOp()很好的控制了HRegion上的多种整体行为,比如flush、compact、split等操作,使其不相互冲突;微观上,针对HRegion上,增加了updatesLock锁,使得数据的更新在flush期间不能进行,保证了数据的准确性;同时,还利用序列号在WAL中标记开始与结束,使得在flush过程中,如果出现异常,系统也能知道开始flush之后数据发生的变化,因为WAL的序列号是递增的,最后,也利用了多版本一致性控制器,保障了写数据时读数据的一致性和完整性,关于多版本一致性控制器相关的内容,将会撰写专门的文章进行介绍,请读者莫急。

        其次,巧妙之处体现在,flush流程采用采用了两把锁,使得Region内部的行为和对外的服务互不影响,同时,利用快照技术,快速生成即将被flush的内存,生成之后立马释放控制写数据的写锁,极大地提高了HBase高并发低延迟的写性能。

        这里,先简单说下写锁和快照的引入,是如何体现HBase高并发写的性能的。

        整个flush的过程是比较繁琐,同时涉及到写真正的物理文件,也是比较耗时的。试想下,如果我们对整个flush过程全程加写锁,结果会怎么样?针对该HRegion的数据读写请求就必须等待整个flush过程的结束,那么对于客户端来说,将不得不经常陷入莫名其妙的等待。

        通过对MemStore生成快照snapshot,并在生成前加更新锁updatesLock的写锁,阻止客户端对MemStore数据的读取与更新,确保了数据的一致性,同时,在快照snapshot生成后,立即释放更新锁updatesLock的写锁,让客户端的后续读写请求与快照flush到物理磁盘文件同步进行,使得客户端的访问请求得到快速的响应,不得不说是HBase团队一个巧妙地设计,也值得我们在以后的系统开发过程中借鉴。

        身体是革命的本钱,不早了,要保证在12点前睡觉啊,还是先休息吧!剩下的细节,只能寄希望于(三)和其他博文了!