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的文件管理器中,對用戶可見。

發佈了87 篇原創文章 · 獲贊 50 · 訪問量 23萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章