繼上篇《HBase源碼分析之HRegion上MemStore的flsuh流程(一)》之後,我們繼續分析下HRegion上MemStore flush的核心方法internalFlushcache(),它的主要流程如圖所示:

其中,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點前睡覺啊,還是先休息吧!剩下的細節,隻能寄希望于(三)和其他博文了!