標籤:分享圖片 問題 時間 EAP 多少 mock 不能 review cspro
【原文】https://www.toutiao.com/i6595365358301872643/
前言
OutOfMemoryError 問題相信很多朋友都遇到過,相對於常見的業務異常(數組越界、null 指標等)來說這類問題是很難定位和解決的。
本文以最近碰到的一次線上記憶體溢出的定位、解決問題的方式展開;希望能對碰到類似問題的同學帶來思路和協助。
主要從表現-->排查-->定位-->解決 四個步驟來分析和解決問題。
表象
最近我們生產上的一個應用不斷的爆出記憶體溢出,並且隨著業務量的增長出現的頻次越來越高。
該程式的商務邏輯非常簡單,就是從 Kafka 中將資料消費下來然後批量的做持久化操作。
而現象則是隨著 Kafka 的訊息越多,出現的異常的頻次就越快。由於當時還有其他工作所以只能讓營運做重啟,並且監控好堆記憶體以及 GC 情況。
重啟大法雖好,可是依然不能根本解決問題。
排查
於是我們想根據營運之前收集到的記憶體資料、GC 日誌嘗試判斷哪裡出現問題。
結果發現老年代的記憶體使用量就算是發生 GC 也一直居高不下,而且隨著時間推移也越來越高。
結合 jstat 的日誌發現就算是發生了 FGC 老年代也已經回收不了,記憶體已經到頂。
甚至有幾台應用 FGC 達到了上百次,時間也高的可怕。
這說明應用的記憶體使用量肯定是有問題的,有許多賴皮對象始終回收不掉。
定位
由於生產上的記憶體 dump 檔案非常大,達到了幾十G。也是由於我們的記憶體設定太大有關。
所以導致想使用 MAT 分析需要花費大量時間。
因此我們便想是否可以在本地複現,這樣就要好定位的多。
為了儘快的複現問題,我將本地應用最大堆記憶體設定為 150M。
然後在消費 Kafka 那裡 Mock 為一個 while 迴圈一直不斷的產生資料。
同時當應用啟動之後利用 VisualVM 連上應用即時監控記憶體、GC 的使用方式。
結果跑了 10 幾分鐘記憶體使用量並沒有什麼問題。根據圖中可以看出,每產生一次 GC 記憶體都能有效回收,所以這樣並沒有複現問題。
沒法複現問題就很難定位了。於是我們 review 代碼,發現生產的邏輯和我們用 while 迴圈 Mock 資料還不太一樣。
查看生產的日誌發現每次從 Kafka 中取出的都是幾百條資料,而我們 Mock 時每次只能產生一條。
為了儘可能的類比生產情況便在伺服器上跑著一個生產者程式,一直源源不斷的向 Kafka 中發送資料。
果然不出意外只跑了一分多鐘記憶體就頂不住了,觀察左圖發現 GC 的頻次非常高,但是記憶體的回收卻是相形見拙。
同時後台也開始列印記憶體溢出了,這樣便複現出問題。
解決
從目前的表現來看就是記憶體中有許多個物件一直存在強參考關聯性導致得不到回收。
於是便想看看到底是什麼對象佔用了這麼多的記憶體,利用 VisualVM 的 HeapDump 功能可以立即 dump 出當前應用的記憶體情況。
結果發現 com.lmax.disruptor.RingBuffer 類型的對象佔用了將近 50% 的記憶體。
看到這個包自然就想到了 Disruptor 環形隊列。
再次 review 代碼發現:從 Kafka 裡取出的 700 條資料是直接往 Disruptor 裡丟的。
這裡也就能說明為什麼第一次類比資料沒複現問題了。
類比的時候是一個對象放進隊列裡,而生產的情況是 700 條資料放進隊列裡。這個資料量是 700 倍的差距。
而 Disruptor 作為一個環形隊列,再對象沒有被覆蓋之前是一直存在的。
我也做了一個實驗,證明確實如此。
我設定隊列大小為 8 ,從 0~9 往裡面寫 10 條資料,當寫到 8 的時候就會把之前 0 的位置覆蓋掉,後面的以此類推(類似於 HashMap 的模數定位)。
所以在生產上假設我們的隊列大小是 1024,那麼隨著系統的運行最終肯定會導致 1024 個位置上裝滿了對象,而且每個位置是 700 個!
於是查看了生產上 Disruptor 的 RingBuffer 配置,結果是:1024*1024。
這個數量級就非常嚇人了。
為了驗證是否是這個問題,我在本地將該值換為 2 ,一個最小值試試。
同樣的 128M 記憶體,也是通過 Kafka 一直源源不斷的取出資料。通過監控如下:
跑了 20 幾分鐘系統一切正常,每當一次 GC 都能回收大部分記憶體,最終呈現鋸齒狀。
這樣問題就找到了,不過生產上這個值具體設定多少還得根據業務情況測試才能知道,但原有的 1024*1024 是絕對不能再使用了。
總結
雖然到了最後也就改了一行代碼(還沒改,直接修改配置),但這排查過程我覺得是有意義的。
也會讓大部分覺得 JVM 這樣的黑盒難以下手的同學有一個直觀的感受。
同時也得感歎 Disruptor 東西雖好,也不能亂用哦!
相關示範代碼查看:
https://github.com/crossoverJie/JCSprout/tree/master/src/main/java/com/crossoverjie/disruptor
【轉】Java學習---記憶體溢出的排查經曆