天天看點

HBase Compaction的前生今世-身世之旅

了解HBase的童鞋都知道,HBase是一種Log-Structured Merge Tree架構模式,使用者資料寫入先寫WAL,再寫緩存,滿足一定條件後緩存資料會執行flush操作真正落盤,形成一個資料檔案HFile。随着資料寫入不斷增多,flush次數也會不斷增多,進而HFile資料檔案就會越來越多。然而,太多資料檔案會導緻資料查詢IO次數增多,是以HBase嘗試着不斷對這些檔案進行合并,這個合并過程稱為Compaction。

Compaction會從一個region的一個store中選擇一些hfile檔案進行合并。合并說來原理很簡單,先從這些待合并的資料檔案中讀出KeyValues,再按照由小到大排列後寫入一個新的檔案中。之後,這個新生成的檔案就會取代之前待合并的所有檔案對外提供服務。HBase根據合并規模将Compaction分為了兩類:MinorCompaction和MajorCompaction

Minor Compaction是指選取一些小的、相鄰的StoreFile将他們合并成一個更大的StoreFile,在這個過程中不會處理已經Deleted或Expired的Cell。一次Minor Compaction的結果是更少并且更大的StoreFile。

Major Compaction是指将所有的StoreFile合并成一個StoreFile,這個過程還會清理三類無意義資料:被删除的資料、TTL過期資料、版本号超過設定版本号的資料。另外,一般情況下,Major Compaction時間會持續比較長,整個過程會消耗大量系統資源,對上層業務有比較大的影響。是以線上業務都會将關閉自動觸發Major Compaction功能,改為手動在業務低峰期觸發。

上文提到,随着hfile檔案數不斷增多,一次查詢就可能會需要越來越多的IO操作,延遲必然會越來越大,如下圖一所示,随着資料寫入不斷增加,檔案數不斷增多,讀取延時也在不斷變大。而執行compaction會使得檔案數基本穩定,進而IO Seek次數會比較穩定,延遲就會穩定在一定範圍。然而,compaction操作重寫檔案會帶來很大的帶寬壓力以及短時間IO壓力。是以可以認為,Compaction就是使用短時間的IO消耗以及帶寬消耗換取後續查詢的低延遲。從圖上來看,就是延遲有很大的毛刺,但總體趨勢基本穩定不變,見下圖二。

HBase Compaction的前生今世-身世之旅
HBase Compaction的前生今世-身世之旅

為了換取後續查詢的低延遲,除了短時間的讀放大之外,Compaction對寫入也會有很大的影響。我們首先假設一個現象:當寫請求非常多,導緻不斷生成HFile,但compact的速度遠遠跟不上HFile生成的速度,這樣就會使HFile的數量會越來越多,導緻讀性能急劇下降。為了避免這種情況,在HFile的數量過多的時候會限制寫請求的速度:在每次執行MemStore flush的操作前,如果HStore的HFile數超過hbase.hstore.blockingStoreFiles (預設7),則會阻塞flush操作hbase.hstore.blockingWaitTime時間,在這段時間内,如果compact操作使得HStore檔案數下降到回這個值,則停止阻塞。另外阻塞超過時間後,也會恢複執行flush操作。這樣做就可以有效地控制大量寫請求的速度,但同時這也是影響寫請求速度的主要原因之一。

可見,Compaction會使得資料讀取延遲一直比較平穩,但付出的代價是大量的讀延遲毛刺和一定的寫阻塞。

了解了一定的背景知識後,接下來需要從全局角度對Compaction進行了解。整個Compaction始于特定的觸發條件,比如flush操作、周期性地Compaction檢查操作等。一旦觸發,HBase會将該Compaction交由一個獨立的線程處理,該線程首先會從對應store中選擇合适的hfile檔案進行合并,這一步是整個Compaction的核心,選取檔案需要遵循很多條件,比如檔案數不能太多、不能太少、檔案大小不能太大等等,最理想的情況是,選取那些承載IO負載重、檔案小的檔案集,實際實作中,HBase提供了多個檔案選取算法:RatioBasedCompactionPolicy、ExploringCompactionPolicy和StripeCompactionPolicy等,使用者也可以通過特定接口實作自己的Compaction算法;選出待合并的檔案後,HBase會根據這些hfile檔案總大小挑選對應的線程池處理,最後對這些檔案執行具體的合并操作。可以通過下圖簡單地梳理上述流程:

HBase Compaction的前生今世-身世之旅

HBase中可以觸發compaction的因素有很多,最常見的因素有這麼三種:Memstore Flush、背景線程周期性檢查、手動觸發。

1. Memstore Flush: 應該說compaction操作的源頭就來自flush操作,memstore flush會産生HFile檔案,檔案越來越多就需要compact。是以在每次執行完Flush操作之後,都會對目前Store中的檔案數進行判斷,一旦檔案數# > ,就會觸發compaction。需要說明的是,compaction都是以Store為機關進行的,而在Flush觸發條件下,整個Region的所有Store都會執行compact,是以會在短時間内執行多次compaction。

2. 背景線程周期性檢查:背景線程CompactionChecker定期觸發檢查是否需要執行compaction,檢查周期為:hbase.server.thread.wakefrequency*hbase.server.compactchecker.interval.multiplier。和flush不同的是,該線程優先檢查檔案數#是否大于,一旦大于就會觸發compaction。如果不滿足,它會接着檢查是否滿足major compaction條件,簡單來說,如果目前store中hfile的最早更新時間早于某個值mcTime,就會觸發major compaction,HBase預想通過這種機制定期删除過期資料。上文mcTime是一個浮動值,浮動區間預設為[7-7*0.2,7+7*0.2],其中7為hbase.hregion.majorcompaction,0.2為hbase.hregion.majorcompaction.jitter,可見預設在7天左右就會執行一次major compaction。使用者如果想禁用major compaction,隻需要将參數hbase.hregion.majorcompaction設為0

3. 手動觸發:一般來講,手動觸發compaction通常是為了執行major compaction,原因有三,其一是因為很多業務擔心自動major compaction影響讀寫性能,是以會選擇低峰期手動觸發;其二也有可能是使用者在執行完alter操作之後希望立刻生效,執行手動觸發major compaction;其三是HBase管理者發現硬碟容量不夠的情況下手動觸發major compaction删除大量過期資料;無論哪種觸發動機,一旦手動觸發,HBase會不做很多自動化檢查,直接執行合并。

選擇合适的檔案進行合并是整個compaction的核心,因為合并檔案的大小以及其目前承載的IO數直接決定了compaction的效果。最理想的情況是,這些檔案承載了大量IO請求但是大小很小,這樣compaction本身不會消耗太多IO,而且合并完成之後對讀的性能會有顯著提升。然而現實情況可能大部分都不會是這樣,在0.96版本和0.98版本,分别提出了兩種選擇政策,在充分考慮整體情況的基礎上選擇最佳方案。無論哪種選擇政策,都會首先對該Store中所有HFile進行一一排查,排除不滿足條件的部分檔案:

1. 排除目前正在執行compact的檔案及其比這些檔案更新的所有檔案(SequenceId更大)

2. 排除某些過大的單個檔案,如果檔案大小大于hbase.hzstore.compaction.max.size(預設Long最大值),則被排除,否則會産生大量IO消耗

經過排除的檔案稱為候選檔案,HBase接下來會再判斷是否滿足major compaction條件,如果滿足,就會選擇全部檔案進行合并。判斷條件有下面三條,隻要滿足其中一條就會執行major compaction:

1. 使用者強制執行major compaction

2. 長時間沒有進行compact(CompactionChecker的判斷條件2)且候選檔案數小于hbase.hstore.compaction.max(預設10)

3. Store中含有Reference檔案,Reference檔案是split region産生的臨時檔案,隻是簡單的引用檔案,一般必須在compact過程中删除

如果不滿足major compaction條件,就必然為minor compaction,HBase主要有兩種minor政策:RatioBasedCompactionPolicy和ExploringCompactionPolicy,下面分别進行介紹:

RatioBasedCompactionPolicy

從老到新逐一掃描所有候選檔案,滿足其中條件之一便停止掃描:

(1)目前檔案大小 < 比它更新的所有檔案大小總和 * ratio,其中ratio是一個可變的比例,在高峰期時ratio為1.2,非高峰期為5,也就是非高峰期允許compact更大的檔案。那什麼時候是高峰期,什麼時候是非高峰期呢?使用者可以配置參數hbase.offpeak.start.hour和hbase.offpeak.end.hour來設定高峰期

(2)目前所剩候選檔案數 <= hbase.store.compaction.min(預設為3)

停止掃描後,待合并檔案就選擇出來了,即為目前掃描檔案+比它更新的所有檔案

ExploringCompactionPolicy

HBase Compaction的前生今世-身世之旅

截止到此,HBase基本上就選擇出來了待合并的檔案集合,後續通過挑選合适的處理線程,就會對這些檔案進行真正的合并 。

HBase實作中有一個專門的線程CompactSplitThead負責接收compact請求以及split請求,而且為了能夠獨立處理這些請求,這個線程内部構造了多個線程池:largeCompactions、smallCompactions以及splits等,其中splits線程池負責處理所有的split請求,largeCompactions和smallCompaction負責處理所有的compaction請求,其中前者用來處理大規模compaction,後者處理小規模compaction。這裡需要明白三點:

1. 上述設計目的是為了能夠将請求獨立處理,提供系統的處理性能。

2. 哪些compaction應該配置設定給largeCompactions處理,哪些應該配置設定給smallCompactions處理?是不是Major Compaction就應該交給largeCompactions線程池處理?不對。這裡有個配置設定原則:待compact的檔案總大小如果大于值throttlePoint(可以通過參數hbase.regionserver.thread.compaction.throttle配置,預設為2.5G),配置設定給largeCompactions處理,否則配置設定給smallCompactions處理。

3. largeCompactions線程池和smallCompactions線程池預設都隻有一個線程,使用者可以通過參數hbase.regionserver.thread.compaction.large和hbase.regionserver.thread.compaction.small進行配置

上文一方面選出了待合并的HFile集合,一方面也選出來了合适的處理線程,萬事俱備,隻欠最後真正的合并。合并流程說起來也簡單,主要分為如下幾步:

1. 分别讀出待合并hfile檔案的KV,并順序寫到位于./tmp目錄下的臨時檔案中

2. 将臨時檔案移動到對應region的資料目錄

3. 将compaction的輸入檔案路徑和輸出檔案路徑封裝為KV寫入WAL日志,并打上compaction标記,最後強制執行sync

4. 将對應region資料目錄下的compaction輸入檔案全部删除

上述四個步驟看起來簡單,但實際是很嚴謹的,具有很強的容錯性和完美的幂等性:

1. 如果RS在步驟2之前發生異常,本次compaction會被認為失敗,如果繼續進行同樣的compaction,上次異常對接下來的compaction不會有任何影響,也不會對讀寫有任何影響。唯一的影響就是多了一份多餘的資料。

2. 如果RS在步驟2之後、步驟3之前發生異常,同樣的,僅僅會多一份備援資料。

3. 如果在步驟3之後、步驟4之前發生異常,RS在重新打開region之後首先會從WAL中看到标有compaction的日志,因為此時輸入檔案和輸出檔案已經持久化到HDFS,是以隻需要根據WAL移除掉compaction輸入檔案即可

本文重點從減少IO的層面對Compaction進行了介紹,其實Compaction還是HBase删除過期資料的唯一手段。文章下半部分着眼于Compaction的整個流程,細化分階段分别進行了梳理。通過本文的介紹,一方面希望讀者對Compaction的左右有一個清晰的認識,另一方面能夠從流程方面了解Compaction的工作原理。然而,Compaction一直是HBase整個架構體系中最重要的一環,對它的改造也從來沒有停止過,改造的重點就是上文的核心點-’選擇合适的HFile合并’,在接下來的一篇文章中會重點分析HBase在此處所作的努力~

本文轉載自:http://hbasefly.com

<a href="http://hbasefly.com/2016/07/13/hbase-compaction-1/" target="_blank">原文連結</a>