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

作者:vivo 互聯網技術團隊

Li Guanyun、 Jessica Chen

一、背景

2021 年 2 月,收到反饋,視頻 APP 某核心接口高峯期響應慢,影響用戶體驗。

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

圖片

圖片

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

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

二、優化目標

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

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

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

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

三、當前存在的問題

當前服務的 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)。

圖片

圖片

四、優化方案 / 驗證方案

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

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

圖片

結合當前服務的實際情況 (堆大小,可維護性),我們選擇 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。其中:

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

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

圖片

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

綜合來看,兩個方案表現十分接近,原則上兩種方案都可以,只是 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 的場景有很多,場景的如下:

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

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

CMS Background GC 由 JVM 的常駐線程定時掃描老年代的使用率,當使用率超過閾值時觸發,該閾值由 - XX:CMSInitiatingOccupancyFraction; 

-XX:+UseCMSInitiatingOccupancyOnly 兩個參數控制,不設置,默認首次爲 92%,後續會根據歷史情況進行預測,動態調整。

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

目標方案的堆分佈如下:

按經驗數據,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 調優的難度。

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

圖片

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_B6yCptCa49xzWNEcYhSDw