高併發場景下JVM調優實踐之路

語言: CN / TW / HK

一、背景

2021年2月,收到反饋,視訊APP某核心介面高峰期響應慢,影響使用者體驗。

通過監控發現,介面響應慢主要是P99耗時高引起的,懷疑與該服務的GC有關,該服務典型的一個例項GC表現如下圖:

可以看出,在觀察週期裡:

  • 平均每10分鐘Young GC次數66次,峰值為470次;

  • 平均每10分鐘Full GC次數0.25次,峰值5次;

可見Full GC非常頻繁,Young GC在特定的時段也比較頻繁,存在較大的優化空間。由於對GC停頓的優化是降低介面的P99時延一個有效的手段,所以決定對該核心服務進行JVM調優。

二、優化目標

  • 介面P99時延降低30%

  • 減少Young GC和Full GC次數、停頓時長、單次停頓時長

由於GC的行為與併發有關,例如當併發比較高時,不管如何調優,Young GC總會很頻繁,總會有不該晉升的物件晉升觸發Full GC,因此優化的目標根據負載分別制定:

目標1:高負載(單機1000 QPS以上)

  • Young GC次數減少20%-30% ,Young GC累積耗時不惡化;

  • Full GC次數減少50%以上,單次、累積Full GC耗時減少50%以上,服務釋出不觸發Full GC。

目標2:中負載(單機500-600)

  • Young GC次數減少20%-30% ,Young GC累積耗時減少20%;

  • Full GC次數不高於4次/天,服務釋出不觸發Full GC。

目標3:低負載(單機200 QPS以下)

  • Young GC次數減少20%-30% ,Young GC累積耗時減少20%;

  • Full GC次數不高於1次/天,服務釋出不觸發Full GC。

三、當前存在的問題

當前服務的JVM配置引數如下:

-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

單純從引數上分析,存在以下問題:

**未顯示指定收集器 **

JDK 8預設蒐集器為ParrallelGC,即Young區採用Parallel Scavenge,老年代採用Parallel Old進行收集,這套配置的特點是吞吐量優先,一般適用於後臺任務型伺服器。

比如批量訂單處理、科學計算等對吞吐量敏感,對時延不敏感的場景,當前服務是視訊與使用者互動的門戶,對時延非常敏感,因此不適合使用預設收集器ParrallelGC,應選擇更合適的收集器。

Young區配比不合理

當前服務主要提供API,這類服務的特點是常駐物件會比較少,絕大多數物件的生命週期都比較短,經過一次或兩次Young GC就會消亡。

再看下當前JVM配置

整個堆為4G,Young區總共1G,預設-XX:SurvivorRatio=8,即有效大小為0.9G,老年代常駐物件大小約400M。

這就意味著,當服務負載較高,請求併發較大時,Young區中Eden + S0區域會迅速填滿,進而Young GC會比較頻繁。

另外會引起本應被Young GC回收的物件過早晉升,增加Full GC的頻率,同時單次收集的區域也會增大,由於Old區使用的是ParralellOld,無法與使用者執行緒併發執行,導致服務長時間停頓,可用性下降, P99響應時間上升。

未設定

-XX:MetaspaceSize和-XX:MaxMetaspaceSize

Perm區在jdk 1.8已經過時,被Meta區取代,
因此-XX:PermSize=512M -XX:MaxPermSize=512M配置會被忽略,
真正控制Meta區GC的引數為
-XX:MetaspaceSize:
Metaspace初始大小,64位機器預設為21M左右
 
-XX:MaxMetaspaceSize:
Metaspace的最大值,64位機器預設為18446744073709551615Byte,
可以理解為無上限
 
-XX:MaxMetaspaceExpansion:
增大觸發metaspace GC閾值的最大要求
 
-XX:MinMetaspaceExpansion:
增大觸發metaspace GC閾值的最小要求,預設為340784Byte

這樣服務在啟動和釋出的過程中,元資料區域達到21M時會觸發一次Full GC (Metadata GC Threshold),隨後隨著元資料區域的擴張,會夾雜若干次Full GC (Metadata GC Threshold),使服務釋出穩定性和效率下降。

此外如果服務使用了大量動態類生成技術的話,也會因為這個機制產生不必要的Full GC (Metadata GC Threshold)。

四、優化方案/驗證方案

上面已分析出當前配置存在的較為明顯的不足,下面優化方案主要先針對性解決這些問題,之後再結合效果決定是否繼續深入優化。

當前主流/優秀的蒐集器包含:

  • Parrallel Scavenge + Parrallel Old:吞吐量優先,後臺任務型服務適合;

  • ParNew + CMS:經典的低停頓蒐集器,絕大多數商用、延時敏感的服務在使用;

  • G1:JDK 9預設蒐集器,堆記憶體比較大(6G-8G以上)的時候表現出比較高吞吐量和短暫的停頓時間;

  • ZGC:JDK 11中推出的一款低延遲垃圾回收器,目前處在實驗階段;

結合當前服務的實際情況(堆大小,可維護性),我們選擇ParNew + CMS方案是比較合適的。

引數選擇的原則如下:

1)Meta區域的大小一定要指定,且MetaspaceSize和MaxMetaspaceSize大小應設定一致,具體多大要結合線上例項的情況,通過jstat -gc可以獲取該服務線上例項的情況。

# jstat -gc 31247
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
37888.0 37888.0 0.0 32438.5 972800.0 403063.5 3145728.0 2700882.3 167320.0 152285.0 18856.0 16442.4 15189 597.209 65 70.447 667.655

可以看出MU在150M左右,因此-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=256M是比較合理的。

2)Young區也不是越大越好

當堆大小一定時,Young區越大,Young GC的頻率一定越小,但Old區域就會變小,如果太小,稍微晉升一些物件就會觸發Full GC得不償失。

如果Young區過小,Young GC就會比較頻繁,這樣Old區就會比較大,單次Full GC的停頓就會比較大。因此Young區的大小需要結合服務情況,分幾種場景進行比較,最終獲得最合適的配置。

基於以上原則,以下為4種引數組合:

1.ParNew +CMS,Young區擴大1倍

-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSScavengeBeforeRemark

**2.ParNew +CMS,**Young區擴大1倍,

去除-XX:+CMSScavengeBeforeRemark(使用【-XX:CMSScavengeBeforeRemark】引數可以做到在重新標記前先執行一次新生代GC)。

因為老年代和年輕代之間的物件存在跨代引用,因此老年代進行GC Roots追蹤時,同樣也會掃描年輕代,而如果能夠在重新標記前先執行一次新生代GC,那麼就可以少掃描一些物件,重新標記階段的效能也能因此提升。)

-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC

3.ParNew +CMS,Young區擴大0.5倍

-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark

4.ParNew +CMS,Young區不變

-Xms4096M -Xmx4096M -Xmn1024M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark

下面,我們需要在壓測環境,對不同負載下4種方案的實際表現進行比較,分析,驗證。

4.1 壓測環境驗證/分析

高負載場景(1100 QPS)GC表現

可以看出,在高負載場景,4種ParNew + CMS的各項指標表現均遠好於Parrallel Scavenge + Parrallel Old。其中:

  • 方案4(Young區擴大0.5倍)表現最佳,介面P95,P99延時相對當前方案降低50%,Full GC累積耗時減少88%, Young GC次數減少23%,Young GC累積耗時減少4%,Young區調大後,雖然次數減少了,但Young區大了,單次Young GC的耗時也大概率會上升,這是符合預期的。

  • Young區擴大1倍的兩種方案,即方案2和方案3,表現接近,介面P95,P99延時相對當前方案降低40%,Full GC累積耗時減少81%, Young GC次數減少43%,Young GC累積耗時減少17%,略遜於Young區擴大0.5倍,總體表現不錯,這兩個方案進行合併,不再區分。

Young區不變的方案在新方案裡,表現最差,淘汰。所以在中負載場景,我們只需要對比方案2和方案4。

中負載場景(600 QPS)GC表現

可以看出,在中負載場景,2種ParNew + CMS(方案2和方案4)的各項指標表現也均遠好於Parrallel Scavenge + Parrallel Old。

  • Young區擴大1倍的方案表現最佳,介面P95,P99延時相對當前方案降低32%,Full GC累積耗時減少93%, Young GC次數減少42%,Young GC累積耗時減少44%;

  • Young區擴大0.5倍的方案稍遜一些。

綜合來看,兩個方案表現十分接近,原則上兩種方案都可以,只是Young區擴大0.5倍的方案在業務高峰期的表現更佳,為儘量保證高峰期服務的穩定和效能,目前更傾向於選擇ParNew + CMS,Young區擴大0.5倍方案。

4.2 灰度方案/分析

為保證覆蓋業務的高峰期,選擇週五、週六、週日分別從兩個機房隨機選擇一臺線上例項,線上例項的指標符合預期後,再進行全量升級。

目標組  xx.xxx.60.6

採用方案2,即目標方案

-Xms4096M -Xmx4096M -Xmn1536M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark

對照組1  xx.xxx.15.215

採用原始方案

-Xms4096M -Xmx4096M -Xmn1024M
-XX:PermSize=512M
-XX:MaxPermSize=512M

對照組2  xx.xxx.40.87

採用方案4,即候選目標方案

-Xms4096M -Xmx4096M -Xmn2048M
-XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize=256M
-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark

灰度3臺機器。

我們先分析下Young GC相關指標:

Young GC次數

Young GC累計耗時

Young GC單次耗時

可以看出,與原始方案相比,目標方案的YGC次數減少50%,累積耗時減少47%,吞吐量提升的同時,服務停頓的頻率大大降低,而代價是單次Young GC的耗時增長3ms,收益是非常高的。

對照方案2即Young區2G的方案整體表現稍遜與目標方案,再分析Full GC指標。

老年代記憶體增長情況

Full GC次數

Full GC累計/單次耗時

與原始方案相比,使用目標方案時,老年代增長的速度要緩慢很多,基本在觀測週期內Full GC發生的次數從155次減少至27次,減少82%,停頓時間均值從399ms減少至60ms,減少85%,毛刺也非常少。

對照方案2即Young區2G的方案整體表現遜於目標方案。到這裡,可以看出,目標方案從各個維度均遠優於原始方案,調優目標也基本達成。

但細心的同學會發現,目標方案相對原始方案,"Full GC"(實際上是CMS Background GC)耗時更加平穩,但每個若干次"Full GC"後會有一個耗時很高的毛刺出現,這意味這個使用者請求在這個時刻會停頓2-3s,能否進一步優化,給使用者一個更加極致的體驗呢?

4.3 再次優化

這裡首先要分析這現象背後的邏輯。

對於CMS蒐集器,採用的蒐集演算法為Mark-Sweep-[Compact]。

CMS蒐集器GC的種類:

CMS Background GC

這種GC是CMS最常見的一類,是週期性的,由JVM的常駐執行緒定時掃描老年代的使用率,當使用率超過閾值時觸發,採用的是Mark-Sweep方式,由於沒有Compact這種耗時操作,且可以與使用者程序並行,所以CMS的停頓會比較低,GC日誌中出現GC (CMS Initial Mark)字樣就代表發生了一次CMS Background GC。

Background GC由於採用的是Mark-Sweep,會導致老年代記憶體碎片,這也是CMS最大的弱點。

CMS Foreground GC

這種GC是CMS蒐集器裡真正意義上的Full GC,採用Serial Old或Parralel Old進行收集,出現的頻率就較低,當往往出現後就會造成較大的停頓。

觸發CMS Foreground GC的場景有很多,場景的如下:

  • System.gc();

  • jmap -histo:live pid;

  • 元資料區域空間不足;

  • 晉升失敗,GC日誌中的標誌為ParNew(promotion failed);

  • 併發模式失敗,GC日誌中的標誌為councurrent mode failure字樣。

不難推斷,目標方案中的毛刺是晉升失敗或併發模式失敗造成的,由於線上沒有開啟列印gc日誌,但也無妨,因為這兩種場景的根因是一致的,就是若干次CMS Backgroud GC後造成的老年代記憶體碎片。

我們只需要儘可能減少由於老年代碎片觸發晉升失敗、併發模式失敗即可。

CMS Background GC由JVM的常駐執行緒定時掃描老年代的使用率,當使用率超過閾值時觸發,該閾值由-XX:CMSInitiatingOccupancyFraction; -XX:+UseCMSInitiatingOccupancyOnly兩個引數控制,不設定,預設首次為92%,後續會根據歷史情況進行預測,動態調整。

如果我們固定閾值的大小,將該閾值設定為一個相對合理的值,既不使GC過於頻繁,又可以降低晉升失敗或併發模式失敗的概率,就可以大大緩解毛刺產生的頻率。

目標方案的堆分佈如下:

  • Young區 1.5G

  • Old區 2.5G

  • Old區常駐物件 約400M

按經驗資料,75%,80%是比較折中的,因此我們選擇-XX:CMSInitiatingOccupancyFraction=75 -

XX:+UseCMSInitiatingOccupancyOnly進行灰度觀察(我們也對80%的場景做了對照實驗,75%優於80%)。

最終目標方案的配置為:

-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark 
-XX:CMSInitiatingOccupancyFraction=75 
-XX:+UseCMSInitiatingOccupancyOnly

如上配置,灰度 xx.xxx.60.6 一臺機器;

從再次優化的結果上看,CMS Foreground GC引起的毛刺基本消失,符合預期。

因此,視訊服務最終目標方案的配置為;

-Xms4096M -Xmx4096M -Xmn1536M 
-XX:MetaspaceSize=256M 
-XX:MaxMetaspaceSize=256M 
-XX:+UseParNewGC 
-XX:+UseConcMarkSweepGC 
-XX:+CMSScavengeBeforeRemark 
-XX:CMSInitiatingOccupancyFraction=75 
-XX:+UseCMSInitiatingOccupancyOnly

五、結果驗收

灰度持續7天左右,覆蓋工作日與週末,結果符合預期,因此符合在線上開啟全量的條件,下面對全量後的結果進行評估。

Young GC次數

Young GC累計耗時

單次Young GC耗時

從Young GC指標上看,調整後Young GC次數平均減少30%,Young GC累積耗時平均減少17%,Young GC單次耗時平均增加約7ms,Young GC的表現符合預期。

除了技術手段,我們也在業務上做了一些優化,調優前例項的Young GC會出現明顯的、不規律的(定時任務不一定分配到當前例項)毛刺,這裡是業務上的一個定時任務,會載入大量資料,調優過程中將該任務進行分片,分攤到多個例項上,進而使Young GC更加平滑。

Full GC單次/累積耗時

從"Full GC"的指標上看,"Full GC"的頻率、停頓極大減少,可以說基本上沒有真正意義上的Full GC了。

核心介面-A (下游依賴較多) P99響應時間,減少19%(從 3457 ms下降至 2817 ms);

核心介面-B (下游依賴中等)  P99響應時間,減少41%(從 1647ms下降至 973ms);

核心介面-C (下游依賴最少) P99響應時間,減少80%(從 628ms下降至 127ms);

綜合來看,整個結果是超出預期的。Young GC表現與設定的目標非常吻合,基本上沒有真正意義上的Full GC,介面P99的優化效果取決於下游依賴的多少,依賴越少,效果越明顯。

六、寫在最後

由於GC演算法複雜,影響GC效能的引數眾多,並且具體引數的設定又取決於服務的特點,這些因素都很大程度增加了JVM調優的難度。

本文結合視訊服務的調優經驗,著重介紹調優的思路和落地過程,同時總結出一些通用的調優流程,希望能給大家提供一些參考。

作者:vivo網際網路技術團隊Li Guanyun、 Jessica Chen