HBase 入門之數據刷寫(Memstore Flush)詳細說明(轉載)

接觸過 HBase 的同學應該對 HBase 寫數據的過程比較熟悉(不熟悉也沒關係)。HBase 寫數據(比如 put、delete)的時候,都是寫 WAL(假設 WAL 沒有被關閉) ,然後將數據寫到一個稱爲 MemStore 的內存結構裏面的,如下圖:

HBase Rowkey 設計指南
但是,MemStore 畢竟是內存裏面的數據結構,寫到這裏面的數據最終還是需要持久化到磁盤的,生成 HFile。如下圖:

HBase Rowkey 設計指南
如果想及時瞭解Spark、Hadoop或者Hbase相關的文章,歡迎關注微信公共帳號:iteblog_hadoop

理解 MemStore 的刷寫對優化 MemStore 有很重要的意義,大部分人遇到的性能問題都是寫操作被阻塞(Block)了,無法寫入HBase。本文基於 HBase 2.0.2,並對 MemStore 的 Flush 進行說明,包括哪幾種條件會觸發 Memstore Flush 及目前常見的刷寫策略(FlushPolicy)。

文章目錄

什麼時候觸發 MemStore Flush

有很多情況會觸發 MemStore 的 Flush 操作,所以我們最好需要了解每種情況在什麼時候觸發 Memstore Flush。總的來說,主要有以下幾種情況會觸發 Memstore Flush:

  • Region 中所有 MemStore 佔用的內存超過相關閾值
  • 整個 RegionServer 的 MemStore 佔用內存總和大於相關閾值
  • WAL數量大於相關閾值
  • 定期自動刷寫
  • 數據更新超過一定閾值
  • 手動觸發刷寫

下面對這幾種刷寫進行簡要說明。

Region 中所有 MemStore 佔用的內存超過相關閾值

當一個 Region 中所有 MemStore 佔用的內存(包括 OnHeap + OffHeap)大小超過刷寫閾值的時候會觸發一次刷寫,這個閾值由 hbase.hregion.memstore.flush.size 參數控制,默認爲128MB。我們每次調用 put、delete 等操作都會檢查的這個條件的。

但是如果我們的數據增加得很快,達到了 hbase.hregion.memstore.flush.size * hbase.hregion.memstore.block.multiplier 的大小,hbase.hregion.memstore.block.multiplier 默認值爲4,也就是128*4=512MB的時候,那麼除了觸發 MemStore 刷寫之外,HBase 還會在刷寫的時候同時阻塞所有寫入該 Store 的寫請求!這時候如果你往對應的 Store 寫數據,會出現 RegionTooBusyException 異常。

整個 RegionServer 的 MemStore 佔用內存總和大於相關閾值

HBase 爲 RegionServer 的 MemStore 分配了一定的寫緩存,大小等於 hbase_heapsize(RegionServer 佔用的堆內存大小)* hbase.regionserver.global.memstore.sizehbase.regionserver.global.memstore.size的默認值是 0.4,也就是說寫緩存大概佔用 RegionServer 整個 JVM 內存使用量的 40%。

如果整個 RegionServer 的 MemStore 佔用內存總和大於 hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize 的時候,將會觸發 MemStore 的刷寫。其中 hbase.regionserver.global.memstore.size.lower.limit 的默認值爲 0.95。

舉個例子,如果我們 HBase 堆內存總共是 32G,按照默認的比例,那麼觸發 RegionServer 級別的 Flush 是 RS 中所有的 MemStore 佔用內存爲:32 * 0.4 * 0.95 = 12.16G。

注意:0.99.0 之前 hbase.regionserver.global.memstore.size 是 hbase.regionserver.global.memstore.upperLimit 參數;hbase.regionserver.global.memstore.size.lower.limit 是 hbase.regionserver.global.memstore.lowerLimit,參見 HBASE-5349

RegionServer 級別的 Flush 策略是每次找到 RS 中佔用內存最大的 Region 對他進行刷寫,這個操作是循環進行的,直到總體內存的佔用低於全局 MemStore 刷寫下
限(hbase.regionserver.global.memstore.size.lower.limit * hbase.regionserver.global.memstore.size * hbase_heapsize)纔會停止。

需要注意的是,如果達到了 RegionServer 級別的 Flush,那麼當前 RegionServer 的所有寫操作將會被阻塞,而且這個阻塞可能會持續到分鐘級別。

WAL數量大於相關閾值

WAL(Write-ahead log,預寫日誌)用來解決宕機之後的操作恢復問題的。數據到達 Region 的時候是先寫入 WAL,然後再被寫到 Memstore 的。如果 WAL 的數量越來越大,這就意味着 MemStore 中未持久化到磁盤的數據越來越多。當 RS 掛掉的時候,恢復時間將會變成,所以有必要在 WAL 到達一定的數量時進行一次刷寫操作。這個閾值(maxLogs)的計算公式如下:

this.blocksize = WALUtil.getWALBlockSize(this.conf, this.fs, this.walDir);

float multiplier = conf.getFloat("hbase.regionserver.logroll.multiplier", 0.5f);

this.logrollsize = (long)(this.blocksize * multiplier);

this.maxLogs = conf.getInt("hbase.regionserver.maxlogs",

      Math.max(32, calculateMaxLogFiles(conf, logrollsize)));

 

public static long getWALBlockSize(Configuration conf, FileSystem fs, Path dir)

      throws IOException {

    return conf.getLong("hbase.regionserver.hlog.blocksize",

        CommonFSUtils.getDefaultBlockSize(fs, dir) * 2);

}

 

private int calculateMaxLogFiles(Configuration conf, long logRollSize) {

    Pair<Long, MemoryType> globalMemstoreSize = MemorySizeUtil.getGlobalMemStoreSize(conf);

    return (int) ((globalMemstoreSize.getFirst() * 2) / logRollSize);

}

也就是說,如果設置了 hbase.regionserver.maxlogs,那就是這個參數的值;否則是 max(32, hbase_heapsize * hbase.regionserver.global.memstore.size * 2 / logRollSize)。如果某個 RegionServer 的 WAL 數量大於 maxLogs 就會觸發 MemStore 的刷寫。

WAL 數量觸發的刷寫策略是,找到最舊的 un-archived WAL 文件,並找到這個 WAL 文件對應的 Regions, 然後對這些 Regions 進行刷寫。

定期自動刷寫

如果我們很久沒有對 HBase 的數據進行更新,這時候就可以依賴定期刷寫策略了。RegionServer 在啓動的時候會啓動一個線程 PeriodicMemStoreFlusher 每隔 hbase.server.thread.wakefrequency 時間去檢查屬於這個 RegionServer 的 Region 有沒有超過一定時間都沒有刷寫,這個時間是由 hbase.regionserver.optionalcacheflushinterval 參數控制的,默認是 3600000,也就是1小時會進行一次刷寫。如果設定爲0,則意味着關閉定時自動刷寫。

爲了防止一次性有過多的 MemStore 刷寫,定期自動刷寫會有 0 ~ 5 分鐘的延遲,具體參見 PeriodicMemStoreFlusher 類的實現。

數據更新超過一定閾值

如果 HBase 的某個 Region 更新的很頻繁,而且既沒有達到自動刷寫閥值,也沒有達到內存的使用限制,但是內存中的更新數量已經足夠多,比如超過 hbase.regionserver.flush.per.changes 參數配置,默認爲30000000,那麼也是會觸發刷寫的。

手動觸發刷寫

除了 HBase 內部一些條件觸發的刷寫之外,我們還可以通過執行相關命令或 API 來觸發 MemStore 的刷寫操作。比如調用可以調用 Admin 接口提供的方法:

void flush(TableName tableName) throws IOException;

void flushRegion(byte[] regionName) throws IOException;

void flushRegionServer(ServerName serverName) throws IOException;

分別對某張表、某個 Region 或者某個 RegionServer 進行刷寫操作。也可以在 Shell 中通過執行 flush 命令:

hbase> flush 'TABLENAME'

hbase> flush 'REGIONNAME'

hbase> flush 'ENCODED_REGIONNAME'

hbase> flush 'REGION_SERVER_NAME'

需要注意的是,以上所有條件觸發的刷寫操作最後都會檢查對應的 HStore 包含的 StoreFiles 文件超過 hbase.hstore.blockingStoreFiles 參數配置的個數,默認值是16。如果滿足這個條件,那麼當前刷寫會被推遲到 hbase.hstore.blockingWaitTime 參數設置的時間後再刷寫。在阻塞刷寫的同時,HBase 還會請求 Split 或 Compaction 操作。

什麼操作會觸發 MemStore 刷寫

我們常見的 put、delete、append、increment、調用 flush 命令、Region 分裂、Region Merge、bulkLoad HFiles 以及給表做快照操作都會對上面的相關條件做檢查,以便判斷要不要做刷寫操作。

MemStore 刷寫策略(FlushPolicy)

在 HBase 1.1 之前,MemStore 刷寫是 Region 級別的。就是說,如果要刷寫某個 MemStore ,MemStore 所在的 Region 中其他 MemStore 也是會被一起刷寫的!這會造成一定的問題,比如小文件問題,具體參見 《爲什麼不建議在 HBase 中使用過多的列族》。針對這個問題,HBASE-10201/HBASE-3149引入列族級別的刷寫。我們可以通過 hbase.regionserver.flush.policy 參數選擇不同的刷寫策略。

目前 HBase 2.0.2 的刷寫策略全部都是實現 FlushPolicy 抽象類的。並且自帶三種刷寫策略:FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy 以及 FlushAllStoresPolicy

FlushAllStoresPolicy

這種刷寫策略實現最簡單,直接返回當前 Region 對應的所有 MemStore。也就是每次刷寫都是對 Region 裏面所有的 MemStore 進行的,這個行爲和 HBase 1.1 之前是一樣的。

FlushAllLargeStoresPolicy

在 HBase 2.0 之前版本是 FlushLargeStoresPolicy,後面被拆分成分 FlushAllLargeStoresPolicyFlushNonSloppyStoresFirstPolicy,參見 HBASE-14920

這種策略會先判斷 Region 中每個 MemStore 的使用內存(OnHeap + OffHeap)是否大於某個閥值,大於這個閥值的 MemStore 將會被刷寫。閥值的計算是由 hbase.hregion.percolumnfamilyflush.size.lower.boundhbase.hregion.percolumnfamilyflush.size.lower.bound.min 以及 hbase.hregion.memstore.flush.size 參數決定的。計算邏輯如下:

//region.getMemStoreFlushSize() / familyNumber

//就是 hbase.hregion.memstore.flush.size 參數的值除以相關表列族的個數

flushSizeLowerBound = max(region.getMemStoreFlushSize() / familyNumber, hbase.hregion.percolumnfamilyflush.size.lower.bound.min)

 

//如果設置了 hbase.hregion.percolumnfamilyflush.size.lower.bound

flushSizeLowerBound = hbase.hregion.percolumnfamilyflush.size.lower.bound

計算邏輯上面已經很清晰的描述了。hbase.hregion.percolumnfamilyflush.size.lower.bound.min 默認值爲 16MB,而 hbase.hregion.percolumnfamilyflush.size.lower.bound 沒有設置。

比如當前表有3個列族,其他用默認的值,那麼 flushSizeLowerBound = max((long)128 / 3, 16) = 42

如果當前 Region 中沒有 MemStore 的使用內存大於上面的閥值,FlushAllLargeStoresPolicy 策略就退化成 FlushAllStoresPolicy 策略了,也就是會對 Region 裏面所有的 MemStore 進行 Flush。

FlushNonSloppyStoresFirstPolicy

HBase 2.0 引入了 in-memory compaction,參見 HBASE-13408。如果我們對相關列族 hbase.hregion.compacting.memstore.type 參數的值不是 NONE,那麼這個 MemStore 的 isSloppyMemStore 值就是 true,否則就是 false。

FlushNonSloppyStoresFirstPolicy 策略將 Region 中的 MemStore 按照 isSloppyMemStore 分到兩個 HashSet 裏面(sloppyStores 和 regularStores)。然後

  • 判斷 regularStores 裏面是否有 MemStore 內存佔用大於相關閥值的 MemStore ,有的話就會對這些 MemStore 進行刷寫,其他的不做處理,這個閥值計算和 FlushAllLargeStoresPolicy 的閥值計算邏輯一致。
  • 如果 regularStores 裏面沒有 MemStore 內存佔用大於相關閥值的 MemStore,這時候就開始在 sloppyStores 裏面尋找是否有 MemStore 內存佔用大於相關閥值的 MemStore,有的話就會對這些 MemStore 進行刷寫,其他的不做處理。
  • 如果上面 sloppyStores 和 regularStores 都沒有滿足條件的 MemStore 需要刷寫,這時候就 FlushNonSloppyStoresFirstPolicy 策略久退化成 FlushAllStoresPolicy 策略了。

刷寫的過程

MemStore 的刷寫過程很複雜,很多操作都可能觸發,但是這些條件觸發的刷寫最終都是調用 HRegion 類中的 internalFlushcache 方法。

protected FlushResultImpl internalFlushcache(WAL wal, long myseqid,

      Collection<HStore> storesToFlush, MonitoredTask status, boolean writeFlushWalMarker,

      FlushLifeCycleTracker tracker) throws IOException {

    PrepareFlushResult result =

        internalPrepareFlushCache(wal, myseqid, storesToFlush, status, writeFlushWalMarker, tracker);

    if (result.result == null) {

      return internalFlushCacheAndCommit(wal, status, result, storesToFlush);

    } else {

      return result.result; // early exit due to failure from prepare stage

    }

}

從上面的實現可以看出,Flush 操作主要分以下幾步做的

  • prepareFlush 階段:刷寫的第一步是對 MemStore 做 snapshot,爲了防止刷寫過程中更新的數據同時在 snapshot 和 MemStore 中而造成後續處理的困難,所以在刷寫期間需要持有 updateLock 。持有了 updateLock 之後,這將阻塞客戶端的寫操作。所以只在創建 snapshot 期間持有 updateLock,而且 snapshot 的創建非常快,所以此鎖期間對客戶的影響一般非常小。對 MemStore 做 snapshot 是 internalPrepareFlushCache 裏面進行的。
  • flushCache 階段:如果創建快照沒問題,那麼返回的 result.result 將爲 null。這時候我們就可以進行下一步 internalFlushCacheAndCommit。其實 internalFlushCacheAndCommit 裏面包含兩個步驟:flushCache 和 commit 階段。flushCache 階段其實就是將 prepareFlush 階段創建好的快照寫到臨時文件裏面,臨時文件是存放在對應 Region 文件夾下面的 .tmp 目錄裏面。
  • commit 階段:將 flushCache 階段生產的臨時文件移到(rename)對應的列族目錄下面,並做一些清理工作,比如刪除第一步生成的 snapshot。

轉載自過往記憶(https://www.iteblog.com/) 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章