純乾貨:內存溢出通過Jprofile排查思路以及實踐總結

嘀嘀嘀~
新鮮出爐的線上bug已到賬,請注意查收!!!!

最近忙的頭都擡不起來,都沒有機會和bug好好說說話;這不線上的bug已經及時趕到,還是內存溢出的。

頭疼的一批,業務都還沒搞完,線上的調用第三方的服務慘遭毒手。

從服務的log日誌上來看是出現了內存溢出,首先分析該服務上一次發佈的內容,嗯!和自己有關(MMP~~)。但是沒有引入什麼大組件,僅僅只是新接入了幾個訪問第三方的接口,按照常理來說應該不會出問題。

好了,既然是內存溢出,那肯定是有大對象出現,這時候看看服務器CPU,1000% 好傢伙~
通過線程ID定位到具體的線程發現是GC出了問題,那麼這次CPU飆高和內存溢出都是因爲服務中出現大對象導致的。

按照之前的處理經驗:

先分析GC次數:


好傢伙,Full GC的真勤快~

老年代幾乎被佔滿了。

通過jmap定位內存實例佔用排名

jmap -histo [pid] | sort -n -r -k 2 | head -10 

不好意思,這裏沒有保留事故現場,裏面列舉出來第一名是AtomicLong。

單單根據這個實例排名沒有發現特別有效的信息,因爲都不是業務裏面類,而是SpringCloud裏面相關的。
這個方法如果發現不是自定義的業務類引起的,就得想辦法生成dump文件去做具體的分析實例相關的引用。

通過dump進行分析

jmap -dump:format=b,file=dump.hprof 15889 // [pid]    // 生成hprof 文件

這裏通過MAT分析該hprof文件的時候出現了問題

mat分析的過程可以參考文中的純乾貨大對象鏈接

java.lang.OutOfMemoryError: GC overhead limit exceeded
    at org.eclipse.mat.hprof.HprofParserHandlerImpl.resolveClassHierarchy(HprofParserHandlerImpl.java:654)
    at org.eclipse.mat.hprof.Pass2Parser.readInstanceDump(Pass2Parser.java:207)
    at org.eclipse.mat.hprof.Pass2Parser.readDumpSegments(Pass2Parser.java:161)
    at org.eclipse.mat.hprof.Pass2Parser.read(Pass2Parser.java:91)
    at org.eclipse.mat.hprof.HprofIndexBuilder.fill(HprofIndexBuilder.java:94)
    at org.eclipse.mat.parser.internal.SnapshotFactoryImpl.parse(SnapshotFactoryImpl.java:222)
    at org.eclipse.mat.parser.internal.SnapshotFactoryImpl.openSnapshot(SnapshotFactoryImpl.java:126)
    at org.eclipse.mat.snapshot.SnapshotFactory.openSnapshot(SnapshotFactory.java:145)
    at org.eclipse.mat.internal.apps.ParseSnapshotApp.parse(ParseSnapshotApp.java:134)
    at org.eclipse.mat.internal.apps.ParseSnapshotApp.start(ParseSnapshotApp.java:106)
    at org.eclipse.equinox.internal.app.EclipseAppHandle.run(EclipseAppHandle.java:196)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.runApplication(EclipseAppLauncher.java:134)
    at org.eclipse.core.runtime.internal.adaptor.EclipseAppLauncher.start(EclipseAppLauncher.java:104)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:388)
    at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:243)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:656)
    at org.eclipse.equinox.launcher.Main.basicRun(Main.java:592)
    at org.eclipse.equinox.launcher.Main.run(Main.java:1498)
    at org.eclipse.equinox.launcher.Main.main(Main.java:1471)

完了,分析不出來!!!!
這個時候我還以爲是linux的mat工具不行了,然後通過壓縮hprof文件,下載到本地然後再進行分析,然而。。。也還是不行!!!

但是!!!!! 由於本人平常喜歡搗鼓一些性能工具,意外在電腦上發現jprofile可以分析該文件。

有了分析的工具那麼其他就好說了,至少有線索,(儘管我對這個工具用的不是很熟~~

關於Jprofile相關的文章資料,有需要可以百度先了解一下大概,還是不清楚可以留言交流

不說了老表上圖!


這個就是工具分析出來的,這裏先定位關鍵信息 :

  • AtomicLong這個實例佔用的總數最多達到了1500W。
  • 其次需要關注的就是MonitorConfig(一開始我也沒注意到這,因爲這些壓根我都沒有咋瞭解過,但是這個類是關鍵

僅僅知道這倆個類是不夠的,因爲還不知道這倆貨是用來幹啥的~ 頭疼.

這個時候只能一步步分析它的引用,由於AtomicLong佔用的最多,那麼需要定位引用它的實例是誰!

選中最大的對象,右鍵選擇Use Retained Objects


選擇References,查看引用,點擊OK。

過程可能需要一些等待 稍安勿躁

分析結果:


這裏面已經列出來的大部分對象,我們只關注他是怎麼產生的、被誰引用的。
選擇第一個對象一直往下找:

請注意這兩個類,從名字上看就是兩個攔截器;
這兩個類都是屬於Cloud體系裏面的用來做監控指標相關的。

然後大概看了下兩個類的代碼:
都是出自:MetricsInterceptorConfiguration配置類加載。
兩個類都是負責攔截請求,根據請求時間、返回狀態碼做彙總。
這裏被開啓的條件是應用中使用了RestTemplate,聯想到溢出的服務上個應用就是對接第三方加入了RestTemplate類,但是問題來了,RestTemplate在我們服務中已經大規模應用都沒有出現過問題,爲什麼會出現內存溢出的情況呢?

看看MetricsClientHttpRequestInterceptor源碼他是怎麼做的:

  1. 首先實現了ClientHttpRequestInterceptor類,在Spring層層處理下,每次使用RestTemplate發起調用都會被該標識的接口實現攔截。
  2. 攔截之後,根據請求的參數進行打標記來標識這一組請求類型,其實就是Map<標記,值>,因爲他要將請求歸類分組!
  3. 所有數據歸類完之後保存到ServoMonitorCache中去,這個類負責統計該URL相關的【最大值、最小值、請求總數】
  4. 既然是Map結構,那麼Key的命中率非常重要,如果命中率非常差,那麼就會命中不了,命中不了就添加,然後值就越來越多!!!

這裏簡單貼一段代碼:

public synchronized BasicTimer getTimer(MonitorConfig config) {
        // 根據外面傳遞進來的config配置,裏面的tags屬性存儲了參數
        BasicTimer t = this.timerCache.get(config);
        if (t != null)
            return t;
        
        t = new BasicTimer(config);
        // 查找不到則進行緩存..
        this.timerCache.put(config, t);

        if (this.timerCache.size() > this.config.getCacheWarningThreshold()) {
            log.warn("timerCache is above the warning threshold of " + this.config.getCacheWarningThreshold() + " with size " + this.timerCache.size() + ".");
        }

        this.monitorRegistry.register(t);
        return t;
    }

看完這個功能之後聯想到之前上線的功能,大概瞭解了溢出的原因:

  • 我們上一次發版的內容就是新加了一個對接第三方的接口,該第三方給到我們的API都是GET請求類型的參數.
  • 然後GET請求URL上面的參數可能是動態的(每次請求參數裏面都帶了時間戳),導致每次攔截器彙總的時候,命中不了,所以一直添加,由於是Map結構又是Spring的單例模式,GC壓根就回收不了這玩意,導致了內存溢出!

其他應用的RestTemplate都沒有出問題的原因是我們都用到是post請求,URL是固定的。不會緩存參數。即便是GET請求參數變化的概率不大!

後面經過本地模擬這種情況證實了想法,誒~ 要了命了。
我也不知道這算不算Bug~

解決的方案也很簡單,通過MetricsInterceptorConfiguration 我們知道它默認是開啓的,但是也可以在配置文件中進行關閉:

spring:
    cloud:
        netflix:
            metrics:
                enabled: false

由於我們壓根都不需要這個類,直接就給關了。後續再繼續關注一下~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章