1. 程式人生 > >HBase最佳實踐-CMS GC調優

HBase最佳實踐-CMS GC調優

HBase發展到當下,對其進行的各種優化從未停止,而GC優化更是其中的重中之重。從0.94版本提出MemStoreLAB策略、Memstore Chuck Pool策略對寫快取Memstore進行優化開始,到0.96版本提出BucketCache以及堆外記憶體方案對讀快取BlockCache進行優化,再到後續2.0版本宣稱會引入更多堆外記憶體,可見HBase會將堆外記憶體的使用作為優化GC的一個戰略方向。然而無論引入多少堆外記憶體,都無法避免讀寫全路徑使用JVM記憶體,就拿BucketCache中offheap模式來講,即使HBase資料塊是快取在堆外記憶體的,但是在讀取的時候還是會首先將堆外記憶體中的block載入到JVM記憶體中,再返回給使用者。可見,無論使用多少堆外記憶體,對JVM記憶體的使用終究是繞不過去,既然繞不過去,就還是需要落腳於GC本身,對GC本身進行優化。本文就將會介紹HBase應用場景下CMS GC策略的調優技巧,後續還會針對另一業界開始使用的GC策略-G1GC策略在HBase應用場景下進行調優介紹。

CMS GC工作原理

如果看官已經對CMS GC工作原理比較熟悉,完全可以跳過本節內容,直接進入下節。如果看官還對CMS GC不是很瞭解,可以參考筆者之前的另一篇文章《HBase GC的前生今生-身世篇》,文中對JVM的記憶體結構以及CMS GC進行了相當詳細的介紹。為了下文介紹方便,在此還是對其中的一些重要知識點進行提煉:

1. 整個JVM記憶體由Young區、Tenured區和Perm區三部分組成,其中Young區又分為一個Eden區和兩個Survivor區

2. 整個物件生命週期簡要說明(一定要爛熟於心,下文會一直用到):

(1)Young區:一個物件初始化之後,首先會進入Eden區,當Eden區滿之後會觸發一次Minor GC,Minor GC會檢查Eden區所有物件是否依舊存活(是否有其他物件引用),如果存活,會將其從Eden區拷貝到Survivor區,並將這些存活物件的age加一,而死亡的物件會被作為垃圾回收。此時Eden區又空閒出來,等新物件填充,填充滿之後再會觸發Minor GC,如此往復。需要注意的是,每執行一次Minor GC,存活物件的age就會加一。

(2)Tenured區:一旦存活物件的age超多一定閾值就會晉升到Tenured區,因此可以理解為Tenured區一般存放長壽物件。很顯然,隨著時間流逝,Tenured區也會被填充滿,此時就會觸發CMS GC(old gc),這種GC相對比較複雜,由5個步驟組成,詳見參考文章。

3. 無論是Minor GC還是CMS GC,都會’Stop-The-World’,即停止使用者的一切執行緒,只留下gc執行緒回收垃圾物件。其中Minor GC的STW時間主要耗費在複製階段,CMS GC的STW時間主要耗費在標示垃圾物件階段。

GC調優目標

上節簡單介紹了Java虛擬機器的記憶體結構以及Java GC的基本知識,接下來會在此基礎上介紹HBase叢集中GC的幾種引數調優技巧。在介紹具體的調優技巧之前,有必要先來看看GC調優的最終目標和基本原則:

1. 平均Minor GC時間儘可能短。因為整個Minor GC都處於STW,因此短時間Minor GC會使使用者讀寫更加平穩,延遲可控。

2. CMS GC次數越少越好。時間越短越好。一方面是因為一次CMS GC一般都會引起至少秒級的應用暫停,對使用者讀寫影響較大;另一方面頻繁的CMS GC會產生大量的記憶體碎片,嚴重的時候會引起Full GC,導致RegionServer宕機。

下面對引數的調優技巧都謹遵以上原則,尤其對於HBase這類延遲敏感性專案而言,在儘量避免嚴重影響使用者讀寫的情況下使得GC更加平穩、暫停時間更短!

CMS GC優化技巧

本節會針對HBase這一應用場景對JVM的各種GC引數進行分析,主要分三個階段進行。第一階段會介紹適用於所有場景下的GC引數配置,這些引數不需要太多解釋讀者就可以輕鬆理解;第二階段和第三階段分別就兩組引數進行調優講解,這兩組引數一般會根據不同的應用場景進行設定才能使得GC效果最好,鑑於這兩組引數的複雜性,我們會通過理論+實驗的方式一一進行說明;

階段一:預設推薦配置

在介紹具體的調優技巧之前,先來看看CMS GC涉及到的所有相關引數及其對應的意義,下面是最常見的引數:

-Xmx -Xms -Xmn -Xss -XX:MaxPermSize= M -XX:+SurvivorRatio=S  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled -XX:+MaxTenuringThreshold=N -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=C -XX:-DisableExplicitGC

通過上文對各個GC引數的說明,可以輕鬆得出第一階段推薦的引數設定如下,這樣的設定基本適用於所有的場景:

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC  -XX:+CMSParallelRemarkEnabled  -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=75% -XX:-DisableExplicitGC

調優預準備

上文通過解釋各個GC引數意義給出了基本的推薦設定,同時也提到幾個對效能影響重大的引數:Xmn、SurvivorRatio以及MaxTenuringThreshold,下面會通過理論推理+實驗驗證的方式對這幾個引數在HBase系統的設定進行調優。在深入介紹調優技巧之前,需要額外針對三個相關部分預先做下講解,這樣可以更好地理解下文的實驗資料分析。這三部分分別是:測試環境+測試基本條件,GC日誌解釋,HBase場景記憶體分析;

測試環境

首先就下文中實驗測試的硬體拓撲、軟體配置以及相關測試資料情況進行說明:


需要強調的是HBase全部配置為BucketCache模式,而不是LruBlockCache。使用了大量堆外記憶體作為讀快取,在很大程度上優化了GC,如下圖:


上圖是在兩種快取策略下GC表現情況,可見BucketCache模式比LruBlockCache模式GC表現好很多,強烈建議線上配置BucketCache模式。可能很多童鞋都測試過這兩種模式下的GC、吞吐量、讀寫延遲等指標,看到測試結果都會很疑惑,BucketCache模式下的各項效能指標都比LruBlockCache差了好多,筆者也疑惑過,後來才明白:測試肯定是在基本全記憶體場景下進行的,這種情況下確實會是如此。讀者可以想想為什麼會如此,實在不明白可以參考之前一篇博文《BlockCache方案效能對比測試報告》。但是話又說回來,在大資料場景下又有多少業務會是全記憶體操作呢?

GC日誌分析

介紹完實驗基本條件後,再對GC日誌進行簡單的解釋,方便下文對日誌進行分析。需要注意只有在新增引數-XX:+PrintTenuringDistribution才能列印對應日誌,強烈建議線上叢集開啟該引數,日誌片段如下:

2016-07-26T10:37:16.933+0800: 227753.150: [GC2016-07-26T10:37:16.933+0800: 227753.150: [ParNew
Desired survivor size 268435456 bytes, new threshold 5 (max 15)
- age   1:   57523184 bytes,   57523184 total
- age   2:   80236520 bytes,  137759704 total
- age   3:   73226496 bytes,  210986200 total
- age   4:   50318392 bytes,  261304592 total
- age   5:   63166384 bytes,  324470976 total
- age   6:        240 bytes,  324471216 total
: 1268903K->305311K(1572864K), 0.0840620 secs] 26598675K->25635082K(66584576K), 0.0844700 secs] [Times: user=1.82 sys=0.08, real=0.08 secs]

上述日誌片段分三部分進行解釋:

第一部分:基本資訊區,主要有兩點需要重點關注,其一是Desired survivor size 268435456 bytes,表示Survivor區大小為256M;其二是new threshold 5 (max 15),表示物件晉級老生代的最大閾值為15,但是因為Survivor區太小導致age大於5的物件會直接溢位晉級老生代(也有可能是閾值設定太大)。

第二部分:不同age物件分佈區,第一列表示該Young區共分佈有age在1~6的物件;第二列表示所在age含有的物件集所佔記憶體大小,比如age為2的所有物件總大小為80236520 bytes;第三列表示小於對應age的所有物件佔用記憶體的累加值,比如age2對應第二列137759704 total表示age為1和age為2的所有物件總大小;

第三部分:記憶體回收資訊區,第一列表示Young區的記憶體回收情況,1268903K->305311K表示Young區回收前記憶體為1268903K,回收後變為305311K;第二列表示Jvm Heap的記憶體回收情況,26598675K->25635082K(66584576K) 表示當前Jvm總分配記憶體為66584576K,回收前物件佔用記憶體為26598675K,回收後物件佔用記憶體為25635082K;第三列表示回收時間,其中real表示本次gc所消耗的STW時間,即使用者業務暫停時間。

HBase場景記憶體分析

通常來講,每種應用都會有自己的記憶體物件特性,分類來說無非就兩種:一種是短壽物件(指存活物件較短的物件,比如臨時變數等)居多工程,比如大多數純HTTP請求處理工程,短壽物件可能佔到所有物件的70%左右;另一種是長壽物件(指存活物件較長的物件,比如TTL設定較長的快取物件)居多工程,比如類似於HBase、Spark等這類大記憶體工程。具體以HBase為例,來看看具體的記憶體物件:

1. RPC請求物件,比如Request物件和Response物件,一般這些物件會隨著短連線RPC的銷燬而消亡,這些物件可以認為是短壽物件;

2. Memstore物件,HBase中Memstore中物件一般會持續存活較長時間,使用者寫入資料到Memstore中之後物件就一直存在,直至Memstore寫滿之後flush到HDFS。一般在寫入QPS較高的情況下寫滿memstore也通常需要一個小時左右,可見Memstore物件肯定是長壽物件。另外,Memstore物件預設比較大,2M大小。

3. BlockCache物件,和Memstore物件一樣,BlockCache物件一般也會在記憶體存活較長時間,屬於長壽物件。這種物件預設64K大小。

因此可以看出,HBase系統屬於長壽物件居多的工程,因此GC的時候只需要將RPC這類短壽物件在Young區淘汰掉就可以達到最好的GC效果。

階段二:NewParSize調優

理論分析

NewParSize表示young區大小,而young區大小直接決定minor gc的頻率。minor gc頻率一方面決定單次minor gc的時間長短,gc越頻繁,gc時間就越短;一方面決定物件晉升到老年代的量,gc越頻繁,晉升到老年代的物件量就越大。解釋起來就是:

1. 增大young區大小,minor gc頻率降低,單次gc時間會較長(young區設定更大,一次gc就需要複製更多物件,耗時必然比較長),業務讀寫操作延遲抖動較大。反之,業務讀寫操作延遲抖動較小,比較平穩。

2. 減小young區大小,minor gc頻率增快,但會加快晉升到老年代的物件總量(每gc一次,物件age就會加一,當age超過閾值就會晉升到老年代,因此gc頻率越高,age就增加越快),潛在增加old gc風險。

因此設定NewParSize需要進行一定的平衡,不能設定太大,也不能設定太小。

實驗結果

實驗條件:分為獨立對照試驗,三臺RegionServer分別設定Xmn為512m、2g、5g,Xmn越大,分配的Young區越大;SurvivorRatio和MaxTenuringThreshold取預設值;

實驗結果曲線圖:


結果分析

1. 圖一是Xmn不同場景下總體的GC耗時曲線圖,其中橫座標表示GC次數,縱座標表示GC耗時(STW),單位ms。需要特別說明的是,這3條曲線是在相同時間段統計的,也就是說在這段時間內Xmn為512m的情況下GC次數最多,而相應的Xmn為5的情況下GC次數最少。

2. 圖一整體上來看綠線尖峰很多而且很高,表示CMS GC較頻繁,但綠線主體部分處於紅線與藍線之下,表示平均Minor GC耗時更短;藍線GC次數最少,尖峰也比較突出,另外Minor GC相比紅線和綠線耗時更長;紅線的Minor GC耗時介於藍線和綠線之間,尖峰比較平穩,表示CMS GC相對比較短暫;因此總體來看,紅線代表的Xmn為2的場景下CMS GC更加合理,平均Minor GC相對不高,而相比之下,另外兩種場景都有特別明顯的缺陷,Xmn=2是一個最優的選擇;圖一隻能直觀上看出這麼多,更加精確結果需要接著看圖二和圖三。

3. 圖二主要統計Minor GC的主要指標:總GC次數以及平均單次Minor GC耗時。兩者來看,更關注後者,因為後者決定了業務讀寫的延遲以及穩定度;由圖中可以看出,Xmn512m的平均單次Minor GC耗時最少,其次是Xmn2g,最差是Xmn5g,達到了130ms左右,意味著在其Minor GC過程中所有業務讀寫延遲至少為130ms;這個也很好理解,Young區越小,Minor GC頻率越高,單次Minor GC需要複製的物件數就越少,耗時越少;

4. 圖三主要統計CMS GC(老年代GC)的主要指標:CMS GC次數以及平均單次老年代GC耗時(只算STW耗時);由圖中可以看出,Xmn2g無論是GC次數還是GC耗時都更加優秀,相比之下Xmn512m就是最差的選擇;解釋起來也很簡單,因為Young區設定太小,Minor GC頻率高,物件age增加很快,很多物件就有可能因為age超過閾值(預設6)晉升到老年代,相對而言會更有可能引入大量短壽物件晉升老年代。而短壽物件相對而言會比較小,比如request、response等,大量小物件一旦進入老年代,就會導致CMS GC的時候需要標註更多物件,必然比較耗時;

實驗結論

可見,測試結果基本和理論分析一致,Xmn設定過小會導致CMS GC效能較差,而設定過大會導致Minor GC效能較差,因此建議在JVM Heap為64g以上的情況下設定Xmn在1~3g之間,在32g之下設定為512m~1g;具體最好經過簡單的線上除錯;需要特別強調的是,筆者在很多場合都看到很多HBase線上叢集會把Xmn設定的很大,比如有些叢集Xmx為48g,Xmn為10g,檢視日誌發現GC效能極差:單次Minor GC基本都在300ms~500ms之間,CMS GC更是很多超過1s。在此強烈建議,將Xmn調大對GC(無論Minor GC還是CMS GC)沒有任何好處,不要設定太大。

階段三:增大Survivor區大小(減小SurvivorRatio) & 增大MaxTenuringThreshold

理論分析

上文講過,一次Minor GC會將存活物件從Eden區(以及survivor from區)複製到Survivor區(to區),因此增大Survivor區可以容納更多的存活物件。這樣就會防止因為Survivor區太小導致很對存活物件還沒有達到MaxTenuringThreshold閾值就直接進入老生代,潛在增大old gc的觸發頻率;但是Survivor區設定太大也會有一定的問題,Survivor設定較大會使得物件可以在Young區’待’的時間很長,但是對於一些長壽物件較多的場景下(比如HBase),大量長壽物件長時間待在Young區做很多’無謂’的複製,一定程度上增加Minor GC開銷。

另外,增加MaxTenuringThreshold相當於提高了進入老年代的門檻,可以有效限制進入老年代的物件數。和Survivor設定相似,調整MaxTenuringThreshold也需要做一個取捨,設定太小會增加CMS GC的觸發頻率以及耗時,而設定太大則會在長壽物件較多場景下增加Minor GC開銷。一般情況下,預設MaxTenuringThreshold=15已經相對比較大,不需要做任何調整。

實驗結果

實驗條件:分為獨立對照試驗,三臺RegionServer分別設定SurvivorRatio為2、8、15,SurvivorRatio越大,Survivor區大小越小;MaxTenuringThreshold取預設值;其他:-Xmx64g,-Xmn2g;

實驗結果曲線:

結果分析

1. 圖一是SurvivorRatio在三種不同場景下對應的GC效能曲線圖,大體可以看出藍線Minor GC次數最多,綠線尖峰太多,即CMS GC效能最差;具體細節再來看圖二和圖三。

2. 圖二主要統計Minor GC主要指標:平均單次Minor GC耗時三者基本相當,SurvivorRatio:2場景下稍微較高,這是因為SurvivorRatio=2對應的Survivor區較大,可以使得物件在Young區’待’的時間很長,在HBase這種長壽物件較多的情況下,可能會增加一些無謂的‘複製’開銷(下文會通過日誌分析詳細解釋)。另外,SurvivorRatio=2場景下Minor GC頻率也比較高,可能的原因是因為在總Young大小確定的情況下,Survivor越大,Eden自然越小,Minor GC頻率就會增大。可見,SurvivorRatio=2場景下Minor GC效能相對稍微較差。

3. 圖三主要統計CMS GC主要指標:三者CMS GC次數基本相當,SurvivorRatio=2場景下單次CMS GC耗時最少,相比SurvivorRatio=8的場景耗時減少30%左右,效能最好;而相比之下SurvivorRatio=15場景下耗時最長,效能相當差;這是因為SurvivorRatio=2場景下存活物件可以長時間待在Young區,可以得到充分的淘汰,晉升到老生代的短壽小物件會比較少,因而CMS GC效能較好;相比SurvivorRatio=15會因為Survivor區設定太小,很多短壽小物件因為得不到充分的淘汰就會‘溢位’到老生代,導致CMS效能很差。

實驗結論

可見,測試結果基本和理論分析也基本一致,對於Minor GC來說,SurvivorRatio設定對其影響不是很大。而對於CMS GC來說,將SurvivorRatio設定過大簡直就是災難,效能極其差。而和預設值SurvivorRatio=8相比,將SurvivorRatio調大有利於短壽小物件更充分地淘汰,因此建議將SurvivorRatio=2

CMS調優結論

1. 快取模式採用BucketCache策略Offheap模式

2. 對於大記憶體(大於64G),採用如下配置:

-Xmx64g -Xms64g -Xmn2g -Xss256k -XX:MaxPermSize=256m -XX:+SurvivorRatio=2  -XX:+UseConcMarkSweepGC -XX:+UseParNewGC 
-XX:+CMSParallelRemarkEnabled -XX:+MaxTenuringThreshold=15 -XX:+UseCMSCompactAtFullCollection  -XX:+UseCMSInitiatingOccupancyOnly        
-XX:CMSInitiatingOccupancyFraction=75 -XX:-DisableExplicitGC

其中Xmn可以隨著Java分配堆記憶體增大而適度增大,但是不能大於4g,取值範圍在1~3g範圍;SurvivorRatio一般建議選擇為2;MaxTenuringThreshold設定為15;

3 對於小記憶體(小於64G),只需要將上述配置中Xmn改為512m-1g即可

總結

本文首先比較系統的介紹了CMS GC的相關知識,之後分三個階段層層推進對HBase叢集中相關重要引數的調優進行了詳細說明,尤其後面兩階段通過理論推理以及實驗驗證的方式對兩組核心引數進行了針對性調整,最終得出一個較為完整的CMS GC引數配置。讀者可以參考該引數配置對叢集進行調整,再通過日誌檢視調整效果~