工程啟動抖動的排查(JIT)
作者:Adrian
來源:SegmentFault 思否社區(qū)
1. 前言
本文僅分享自己在工作中遇到的問題時的解決方案和思路,以及排查的過程。重點還是分享排查的思路,知識點其實已經(jīng)挺老了。如有疑問或描述不妥,歡迎賜教。
2. 問題表象
在工程啟動的時候,系統(tǒng)的請求會有一波超時,從監(jiān)控來看,JVM 的GC(G1) 波動較大,CPU波動較大,各個業(yè)務使用的線程池波動較大,外部IO耗時增加。
3. 先說結(jié)論
由于JIT的優(yōu)化,導致系統(tǒng)啟動時觸發(fā)了熱點代碼的編譯,且為C2編譯,引發(fā)了CPU占用較高,進而引發(fā)一系列問題,最終導致部分請求超時。
4. 排查過程
4.1 最初的排查
其實我們的工程是一個算法排序工程,里面或多或少也加了一些小的模型和大大小小的緩存,而且從監(jiān)控上來看,JVM 的GC 突刺和 CPU 突刺時間極為接近(這也是一個監(jiān)控平臺時間不夠精準的原因)。所以在前期,我耗費了大量精力和時間去排查JVM,GC 的問題。
首先推薦給大家一個網(wǎng)站:https://gceasy.io/ ,真的分析GC日志巨好用。配合以下的JVM參數(shù)打印GC日志:
-XX:+PrintGC 輸出GC日志
-XX:+PrintGCDetails 輸出GC的詳細日志
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式,你啟動的時候相當于12點,跟真實時間無關(guān))
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-Xloggc:../logs/gc.log 日志文件的輸出路因為看到Y(jié)GC嚴重,所以先后嘗試了如下的方法:
調(diào)整JVM 的堆大小。即 -Xms, -Xmx 參數(shù)。無效。
調(diào)整回收線程數(shù)目。即 -XX:ConcGCThreads 參數(shù)。無效。
調(diào)整期望單次回收時間。即 -XX:MaxGCPauseMillis 參數(shù),無效,甚至更慘。
以上調(diào)整混合測試,均無效。
雞賊的方法。在加載模型之后sleep 一段時間,讓GC平穩(wěn),然后再放請求進來,這樣操作之后GC確實有些好轉(zhuǎn),但是剛開始的請求仍然有超時。(當然了,因為問題根本不在GC上)
4.2 換個思路
根據(jù)監(jiān)控上來看,線程池,外部IO,啟動時都有明顯的RT上升然后下降,而且趨勢非常一致,這種一般都是系統(tǒng)性問題造成的,比如CPU,GC,網(wǎng)卡,云主機超售,機房延遲等等。所以GC既然無法根治,那么就從CPU方面入手看看。
因為系統(tǒng)啟動時JVM會產(chǎn)生大量GC,無法區(qū)分是由于系統(tǒng)啟動還沒預熱好就來了流量還是說無論啟動了多久,流量一來就會出問題。而我之前排查GC 的操作,即加sleep時間,恰好幫我看到了這個問題,因為能明顯的看出,GC波動的時間,和超時的時間,時間點上已經(jīng)差了很多了,那就是說,與GC無關(guān),無論GC已經(jīng)多么平穩(wěn),流量一來,還是要超時。
4.3 分析利器Arthas
Arthas 文檔:
https://arthas.aliyun.com/doc/quick-start.html
其實要分析的核心還是流量最開始到來的時候,我們的CPU到底做了什么,于是我們使用Arthas分析流量到來時的CPU情況。其實這部分也可以使用top -Hp pid , jstack 等命令配合完成,不展開敘述。
CPU情況,僅展示重要部分:

4.4 問題的核心
那么這個C2 CompilerThread 究竟是什么呢。
《深入理解JAVA虛擬機》其實有對這部分的敘述,這里我就大白話給大家解釋一下。
其實Java在最開始運行的時候,你可以理解為,就是傻乎乎的按照你寫的代碼執(zhí)行下去,稱之為"解釋器",這樣有一個好處,就是很快,Java搞成.class ,很快就能啟動,跑起來了,但是問題也很明顯啊,就是運行的慢,那么聰明的JVM開發(fā)者們做了一件事情,他們?nèi)绻l(fā)現(xiàn)你有一些代碼頻繁的執(zhí)行,那么他們就會在運行期間幫你把這段代碼編譯成機器碼,這樣運行就會飛快,這就是即時編譯(just-in-time compilation 也就是JIT)。但是這樣也有一個問題,就是編譯的那段時間,耗費CPU。而C2 CompilerThread,正是JIT中的一層優(yōu)化(共計五層,C2 是第五層)。所以,罪魁禍首找到了。
5. 嘗試解決
解釋器和編譯器的關(guān)系可以如下所示:

就像上面說的,解釋器啟動快,但是執(zhí)行慢。而編譯器又分為以下五個層次。
第 0 層:程序解釋執(zhí)行,默認開啟性能監(jiān)控功能(Profiling),如果不開啟,可觸發(fā)第二層編譯;
第 1 層:可稱為 C1 編譯,將字節(jié)碼編譯為本地代碼,進行簡單、可靠的優(yōu)化,不開啟 Profiling;
第 2 層:也稱為 C1 編譯,開啟 Profiling,僅執(zhí)行帶方法調(diào)用次數(shù)和循環(huán)回邊執(zhí)行次數(shù) profiling 的 C1 編譯;
第 3 層:也稱為 C1 編譯,執(zhí)行所有帶 Profiling 的 C1 編譯;
第 4 層:可稱為 C2 編譯,也是將字節(jié)碼編譯為本地代碼,但是會啟用一些編譯耗時較長的優(yōu)化,甚至會根據(jù)性能監(jiān)控信息進行一些不可靠的激進優(yōu)化所以我們可以嘗試從C1,C2編譯器的角度去解決問題。
5.1 關(guān)閉分層編譯
增加參數(shù) :-XX:-TieredCompilation -client (關(guān)閉分層編譯,開啟C1編譯)
效果稀爛。
CPU使用率持續(xù)高水位(相比于調(diào)整前)。確實沒了C2 thread 的問題,但是猜測由于代碼編譯的不夠C2那么優(yōu)秀,所以代碼持續(xù)性能低下。
CPU截圖:

5.2 增加C2 線程數(shù)
增加參數(shù) :-XX:CICompilerCount=8 恢復參數(shù):-XX:+TieredCompilation
效果一般,仍然有請求超時。但是會少一些。
CPU截圖:

5.3 推論
其實從上面的分析可以看出,如果繞不過C2,那么必然會有一些抖動,如果繞過了C2,那么整體性能就會低很多,這是我們不愿看見的,所以關(guān)閉C1,C2,直接以解釋器模式運行我并沒有嘗試。
5.4 最終方案
既然這部分抖動繞不過去,那么我們可以使用一些mock 流量來承受這部分抖動,也可以稱之為預熱,在工程啟動的時候,使用提前錄制好的流量來使系統(tǒng)熱點代碼完成即時編譯,然后再接收真正的流量,這樣就可以做到真實流量不抖動的效果。
后話
本文著重分享解決和分析的過程,知識點沒有重點分析。更多知識點請看“參考文章”部分。 本文如有問題歡迎各位校正。
參考文章
【關(guān)于java:-XX:-TieredCompilation到底做什么】https://www.codenong.com/38721235/ 【好像是上面那篇文章的原版】 https://stackoverflow.com/questions/38721235/what-exactly-does-xx-tieredcompilation-do 【C2 Compiler Thread】 https://blog.csdn.net/chenxiusheng/article/details/74007750 【C2 CompilerThread9 長時間占用CPU解決方案】https://blog.csdn.net/m0_37886429/article/details/105139611 《深入理解Java虛擬機第二版》第四部分的“晚期(運行期)優(yōu)化” 【深入分析JVM中線程的創(chuàng)建和運行原理 || JIT(future)】 https://www.cnblogs.com/silyvin/p/10228184.html 【HotSpot虛擬機的分層編譯(Tiered Compilation)】 https://blog.csdn.net/u013490280/article/details/108522427

