作者:京東科技 韓國(guó)凱
一、問(wèn)題發(fā)現(xiàn)與排查
1.1 找到問(wèn)題原因
問(wèn)題起因是我們收到了jdos的容器CPU告警,CPU使用率已經(jīng)達(dá)到104%
觀察該機(jī)器日志發(fā)現(xiàn),此時(shí)有很多線(xiàn)程在執(zhí)行跑批任務(wù)。正常來(lái)說(shuō),跑批任務(wù)是低CPU高內(nèi)存型,所以此時(shí)考慮是FullGC引起的大量CPU占用(之前有類(lèi)似情況,告知用戶(hù)后重啟應(yīng)用后解決問(wèn)題)。
通過(guò)泰山查看該機(jī)器內(nèi)存使用情況:
可以看到CPU確實(shí)使用率偏高,但是內(nèi)存使用率并不高,只有62%,屬于正常范圍內(nèi)。
到這里其實(shí)就有點(diǎn)迷惑了,按道理來(lái)說(shuō)此時(shí)內(nèi)存應(yīng)該已經(jīng)打滿(mǎn)才對(duì)。
后面根據(jù)其他指標(biāo),例如流量的突然進(jìn)入也懷疑過(guò)是jsf接口被突然大量調(diào)用導(dǎo)致的cpu占滿(mǎn),所以?xún)?nèi)存使用率不高,不過(guò)后面都慢慢排除了。其實(shí)在這里就有點(diǎn)一籌莫展了,現(xiàn)象與猜測(cè)不符,只有CPU增長(zhǎng)而沒(méi)有內(nèi)存增長(zhǎng),那么什么原因會(huì)導(dǎo)致單方面CPU增長(zhǎng)?然后又朝這個(gè)方向排查了半天也都被否定了。
后面突然意識(shí)到,會(huì)不會(huì)是監(jiān)控有“問(wèn)題”?
換句話(huà)說(shuō)應(yīng)該是我們看到的監(jiān)控有問(wèn)題,這里的監(jiān)控是機(jī)器的監(jiān)控,而不是JVM的監(jiān)控!
JVM的使用的CPU是在機(jī)器上能體現(xiàn)出來(lái)的,而JVM的堆內(nèi)存高額使用之后在機(jī)器上體現(xiàn)的并不是很明顯。
遂去sgm查看對(duì)應(yīng)節(jié)點(diǎn)的jvm相關(guān)情況:
可以看到我們的堆內(nèi)存老年代確實(shí)有過(guò)被打滿(mǎn)然后又清理后的情況,查看此時(shí)的CPU使用情況也可以與GC時(shí)間對(duì)應(yīng)上。
那么此時(shí)可以確定,是Full GC引起的問(wèn)題。
1.2 找到FULL GC的原因
我們首先dump出了gc前后的堆內(nèi)存快照,
然后使用JPofiler進(jìn)行內(nèi)存分析。(JProfiler是一款堆內(nèi)存分析工具,可以直接連接線(xiàn)上jvm實(shí)時(shí)查看相關(guān)信息,也可以分析dump出來(lái)的堆內(nèi)存快照,對(duì)某一時(shí)刻的堆內(nèi)存情況進(jìn)行分析)
首先將我們dump出來(lái)的文件解壓,修改后綴名.bin
,然后打開(kāi)即可。(我們使用行云上自帶的dump小工具,也可以自己去機(jī)器上通過(guò)命令手工dump文件)
首先選擇Biggest Objects,查看當(dāng)時(shí)堆內(nèi)存中最大的幾個(gè)對(duì)象。
從圖中可以看出,四個(gè)List對(duì)象就占據(jù)了近900MB的內(nèi)存,而我們剛剛看到堆內(nèi)存最大也只有1.3GB,因此再加上其他的對(duì)象,很容易就會(huì)把老年代占滿(mǎn)引發(fā)full gc的問(wèn)題。
選擇其中一個(gè)最大的對(duì)象作為我們要查看的對(duì)象
這個(gè)時(shí)候我們已經(jīng)可以定位到對(duì)應(yīng)的大內(nèi)存對(duì)象對(duì)應(yīng)的位置:
其實(shí)至此我們已經(jīng)能夠大概定位出問(wèn)題所在,如果還是不確定的話(huà),可以查看具體的對(duì)象信息,方法如下:
可以看到我們的大List對(duì)象,其實(shí)內(nèi)部是很多個(gè)Map對(duì)象,而每個(gè)Map對(duì)象中又有很多鍵值對(duì)。
在這里也可以看到Map中的相關(guān)屬性信息。
也可以在以下界面直接看到相關(guān)信息:
然后一路點(diǎn)下去就可以看到對(duì)應(yīng)的屬性。
至此,我們理論上已經(jīng)找到了大對(duì)象在代碼中的位置。
二、問(wèn)題解決
2.1 找到大對(duì)象在代碼中的位置與問(wèn)題的根本原因
首先我們根據(jù)上述過(guò)程找到對(duì)應(yīng)位置與邏輯
我們的項(xiàng)目中大概邏輯是這樣的:
- 首先會(huì)解析用戶(hù)上傳的Excel樣本,并將其加載到內(nèi)存中作為一個(gè)List變量,即我們上述看到的變量。一個(gè)20w的樣本,此時(shí)字段數(shù)量有a個(gè),大概占用空間100mb左右。
- 然后遍歷循環(huán)用戶(hù)樣本,根據(jù)用戶(hù)樣本中的數(shù)據(jù),再增加一些額外的請(qǐng)求數(shù)據(jù),根據(jù)此數(shù)據(jù)請(qǐng)求相關(guān)結(jié)果。此時(shí)字段數(shù)量有a+n個(gè),占用空間已經(jīng)在200mb左右。
- 循環(huán)完成后將此200mb的數(shù)據(jù)存入緩存。
- 開(kāi)始生成excel,將200mb數(shù)據(jù)從緩存中取出,并根據(jù)之前記錄的a個(gè)字段,取出初始的樣本字段填充至excel。
用流程圖表示為:
結(jié)合一些具體排查問(wèn)題的圖片:
其中一個(gè)現(xiàn)象是每次gc后的最小內(nèi)存正在逐步變大,對(duì)應(yīng)上述步驟中第二步,內(nèi)存正在逐步膨脹。
結(jié)論:
將用戶(hù)上傳的excel樣本加載到內(nèi)存中,并將其作為一個(gè)List<Map<String, String>>
的結(jié)構(gòu)存儲(chǔ)起來(lái),首先一個(gè)20mb的excel文件以此方式存儲(chǔ)會(huì)膨脹占用120mb左右堆內(nèi)存,此步驟會(huì)大量占用堆內(nèi)存,并且因?yàn)槿蝿?wù)邏輯原因,該大對(duì)象內(nèi)存會(huì)在jvm中存在長(zhǎng)達(dá)4-12小時(shí)之久,導(dǎo)致一但任務(wù)過(guò)多,jvm堆內(nèi)存很容易被打滿(mǎn)。
這里列舉了為什么使用HashMap會(huì)導(dǎo)致內(nèi)存膨脹,其主要原因是存儲(chǔ)空間效率比較低:
一個(gè)Long對(duì)象占內(nèi)存計(jì)算:在HashMap<Long,Long>結(jié)構(gòu)中,只有Key和Value所存放的兩個(gè)長(zhǎng)整型數(shù)據(jù)是有效數(shù)據(jù),共16字節(jié)(2×8字節(jié))。這兩個(gè)長(zhǎng)整型數(shù)據(jù)包裝成java.lang.Long對(duì)象之后,就分別具有8字節(jié)的MarkWord、8字節(jié)的Klass指針,再加8字節(jié)存儲(chǔ)數(shù)據(jù)的long值(一個(gè)包裝對(duì)象占24字節(jié))。
然后這2個(gè)Long對(duì)象組成Map.Entry之后,又多了16字節(jié)的對(duì)象頭(8字節(jié)MarkWord+8字節(jié)Klass指針=16字節(jié)),然后一個(gè)8字節(jié)的next字段和4字節(jié)的int型的hash字段(8字節(jié)next指針+4字節(jié)hash字段+4字節(jié)填充=16字節(jié)),為了對(duì)齊,還必須添加4字節(jié)的空白填充,最后還有HashMap中對(duì)這個(gè)Entry的8字節(jié)的引用,這樣增加兩個(gè)長(zhǎng)整型數(shù)字,實(shí)際耗費(fèi)的內(nèi)存為(Long(24byte)×2)+Entry(32byte)+HashMapRef(8byte)=88byte,空間效率為有效數(shù)據(jù)除以全部?jī)?nèi)存空間,即16字節(jié)/88字節(jié)=18%。
——《深入理解Java虛擬機(jī)》5.2.6
以下是剛上傳的excel中dump出的堆內(nèi)存對(duì)象,其占用的內(nèi)存達(dá)到了128mb,而上傳的excel實(shí)際只有17.11mb。
空間效率17.1mb/128mb≈13.4%
2.2 如何解決此問(wèn)題
暫且不討論上述流程是否合理,解決辦法一般可以分為兩類(lèi),一類(lèi)是治本,即不把該對(duì)象放入jvm內(nèi)存中,轉(zhuǎn)而存入緩存中,不在內(nèi)存中則大對(duì)象問(wèn)題自然迎刃而解。另一類(lèi)是治標(biāo),即縮小該大內(nèi)存對(duì)象,在日常使用場(chǎng)景下使其一般不會(huì)觸發(fā)頻繁的full gc問(wèn)題。
兩種方式各有優(yōu)劣:
2.2.1 激進(jìn)治療:不把他存入內(nèi)存
解決邏輯也很簡(jiǎn)單,例如在加載數(shù)據(jù)時(shí),將其按照樣本加載數(shù)據(jù)一條一條存入redis緩存,然后我們只需要知道樣本中有多少的數(shù)量,按照數(shù)量的先后順序從緩存中取出數(shù)據(jù),即可解決該問(wèn)題。
優(yōu)點(diǎn):可以從根本上解決此問(wèn)題,以后基本上不會(huì)存在該問(wèn)題,數(shù)據(jù)量再大只需要添加相應(yīng)的redis資源即可。
缺點(diǎn):首先會(huì)增加許多redis緩存空間消耗,其次從顯示考慮對(duì)于我們項(xiàng)目來(lái)說(shuō),此處代碼古老且晦澀難懂,改動(dòng)需要較大工作量與回歸測(cè)試。
2.2.2 保守治療:縮減其數(shù)據(jù)量
分析2.1的上述流程,首先第三步是完全沒(méi)必要的,先存入緩存再取出,額外占用緩存空間。(猜測(cè)系歷史問(wèn)題,此處不再深究)。
其次是在第二步中,多出來(lái)的字段n,在請(qǐng)求結(jié)束后該字段就已經(jīng)無(wú)用了,因此可以考慮在請(qǐng)求結(jié)束后刪除無(wú)用字段。
此時(shí)也有兩種解決方案,一種是只刪除無(wú)用字段縮減其map大小,然后將其作為參數(shù)傳遞給生成excel使用;另一種方式是請(qǐng)求完成直接刪除該map,然后在生成excel時(shí)再重新讀取用戶(hù)上傳的excel樣本。
優(yōu)點(diǎn):改動(dòng)較小,不需要太復(fù)雜的回歸測(cè)試
缺點(diǎn):在極端大數(shù)據(jù)量情況下,仍有可能出現(xiàn)full gc的情況
具體實(shí)現(xiàn)方式就不展開(kāi)了。
其中一種實(shí)現(xiàn)方式
//獲取有用的字段
String[] colEnNames = (String[]) colNameMap.get(Constant.BATCH_COL_EN_NAMES);
List<String> colList = Arrays.asList(colEnNames);
//去除無(wú)用的字段
param.keySet().removeIf(key -> !colList.contains(key));
三、拓展思考
首先本文中監(jiān)控圖是在復(fù)現(xiàn)當(dāng)時(shí)場(chǎng)景時(shí)人為制造的gc常見(jiàn)。
在cpu使用率圖中,大家可以觀察到cpu使用率上升時(shí)間確實(shí)跟gc的時(shí)間相吻合,但是并沒(méi)有出現(xiàn)當(dāng)時(shí)場(chǎng)景中的104%的CPU使用率。
其實(shí)直接原因比較簡(jiǎn)單,就是因?yàn)橄到y(tǒng)雖然出現(xiàn)了full gc,但是并沒(méi)有頻繁出現(xiàn)。
小范圍低頻率的full gc不太會(huì)引起系統(tǒng)的cpu飆升,這也是我們所看到的現(xiàn)象。
那么當(dāng)時(shí)的場(chǎng)景是什么原因呢?
我們上文提到過(guò),我們?cè)?strong>堆內(nèi)存中的大對(duì)象是會(huì)隨著任務(wù)的進(jìn)行逐步膨脹的,那么當(dāng)我們的任務(wù)足夠多,時(shí)間足夠長(zhǎng),就有可能導(dǎo)致每次full gc后可用空間變得越來(lái)越小,當(dāng)可用空間小到一定程度之后就,每次full gc完成之后發(fā)現(xiàn)空間還是不夠使用,就會(huì)觸發(fā)下一次的gc,從而導(dǎo)致最終結(jié)果的頻繁發(fā)生gc,引起cpu頻率的飆升不下。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-434236.html
四、問(wèn)題排查總結(jié)
- 當(dāng)我們遇到線(xiàn)上cpu使用率過(guò)高的情況時(shí),可以先查看是否是full gc引起的問(wèn)題,注意要看的是jvm的監(jiān)控,或者使用jstat相關(guān)命令查看。不要被機(jī)器內(nèi)存監(jiān)控所誤導(dǎo)。
- 如果確定是gc引起的問(wèn)題,可以通過(guò)JProfiler直連線(xiàn)上jvm或者使用dump保存堆快照后離線(xiàn)分析。
- 首先可以找到最大的對(duì)象,一般情況下是大對(duì)象引起的full gc。還有一種情況是,不像這么明顯是四個(gè)大對(duì)象,也可能是比較均衡的十幾個(gè)50mb的對(duì)象,具體情況還需要具體分析。
- 通過(guò)上述工具找到確定有問(wèn)題的對(duì)象后找到其堆棧對(duì)應(yīng)的代碼位置,通過(guò)代碼分析找到問(wèn)題的具體原因,通過(guò)其他現(xiàn)象推演猜測(cè)是否正確,進(jìn)而找到問(wèn)題的真正原因。
- 根據(jù)問(wèn)題的原因解決此問(wèn)題。
當(dāng)然,上述只是不算很復(fù)雜的排查情況,不同的系統(tǒng)肯定有不同的內(nèi)存情況,我們應(yīng)當(dāng)具體問(wèn)題具體分析,而從此次問(wèn)題中可以學(xué)到的就是如果排查解決問(wèn)題的思路。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-434236.html
到了這里,關(guān)于線(xiàn)上FullGC問(wèn)題排查實(shí)踐——手把手教你排查線(xiàn)上問(wèn)題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!