java內存分析工具-jmap/jstat/jvisual vm/mat -- 記一次Flink任務OOM問題的解決

背景:最近用到flink做項目,程序在線上遇到了內存持續增長最後導致OOM的問題;還有一種情況是內存增長過高,在觸發GC的時候產生超長停頓使taskmanager失去心跳而導致任務失敗。

 

OOM問題比較難查,幸虧有團隊的小夥伴一起幫着查詢並解決。記錄一下查詢過程和中間用到的內存分析工具使用方法,以前很少用到這些工具,現在頻繁使用,發覺其實也不是那麼高深莫測。

堆dump

首先,對於內存超高的進程進行堆dump。命令如下:

jmap -dump:format=b,file=xxx pid 

jvisualvm查看堆文件

拿到dump文件後,嘗試用jhat進行加載查看,但jhat是着實難用。後來用jvisualvm來查看,用一下命令指定其內存大小,不然有可能會出現內存過小沒法加載dump文件的情況。

jvisualvm -J-Xms2048M -J-Xmx6144M

裝入後可以看到內存中有哪些類的對象,對象的個數和大小。這個在網上也能找到更多的介紹,這裏先不做說明了,因爲visual vm也不好用,最好用的還是mat。

mat查看堆文件

mat是Memory Analyzer Tool的簡稱,本來是eclipse的一個插件,也可以單獨下載這個工具使用。

下載完後,修改MemoryAnalyzer.ini配置文件,將內存改成-Xmx6144m  (根據實際情況修改)

啓動mat,加載堆文件,它會解析這個堆文件並生成很多的解析後結果,下次加載的時候就不會解析了,直接讀取以前的解析結果,非常方便。加載後的首頁結果如下:

本次主要用到了histogram,打開後如下圖:

四列對應的關係爲:

class name:類名

objects:該類有多少個實例

shallow heap:該類本身佔用的內存大小,不包括它所引用的對象

retained heap:該類和它直接/間接引用的對象所佔的內存總大小。

根據retained heap能看到程序中相關的數據是哪個佔用內存比較大,然後定位到是flink中做join時兩份數據的其中一份佔用內存很高,同時追溯是誰引用了這個對象導致其不釋放持續增長。

根據圖中的關係發現是一個cogroup處的TaggedUnion直接引用了數據(這個沒截圖出來),然後再繼續追溯發現有一些state操作,看來flink的窗口會把緩存的數據都當做狀態數據,然後這裏同時解釋了之前的一個疑惑:爲什麼開啓checkpoint的時候,會產生大io,因爲這裏的數據也是狀態數據,也會進行checkpoint。

OK,發現了是在做cogroup的時候會緩存數據,但這個數據按道理應該在使用完後會釋放纔對(這個後來又在本機做了個實驗,發現是可以GC回收的),爲什麼內存持續增長至OOM呢?(在線上也找到了某個進程,手動觸發GC,發現內存也沒有釋放。觸發GC的方法:jcmd pid GC.run)

然後想到,可能是在做cogroup的時候,有一個流的數據沒來,導致單個流的數據持續積累,而且無法釋放,最終把內存打滿。

 

如何解決雙流join時,一個流的數據不來使流程卡死的問題呢

1、使用window的trigger(不可行)

trigger是window的一個必備組件。

window有assigner、trigger、evictor三個組件,分別負責將數據分配到指定的窗口、觸發窗口計算、窗口計算前過濾一些數據。

trigger可以根據任意自定義條件觸發窗口,觸發入口有兩個:onElement()方法和onEventTime()方法(程序指定了使用事件時間),onElement是每個數據到來的時候都會觸發這個方法,onEventTime是當watermark超過窗口最大時間的時候會觸發。

爲什麼不可行:

(1)如果是onElement方法觸發窗口計算,前提是數據已經被分配到這個窗口內了,如果不來數據怎麼進行觸發呢?

(2)如果是onEventTime方法觸發窗口計算,這個前提是watermark已經做了更新。cogroup窗口的watermark取其兩個數據源中watermark的最小值,如果一個流不來數據,那麼窗口的watermark永遠不能得到更新

(3)窗口觸發計算不是目的,目的是窗口的清理,需要保證在窗口觸發後能即時清理窗口。兩個方法:觸發窗口時同時清理數據,也就是在上面兩個方法中返回TriggerResult.FIRE_AND_PURGE;實現clear()方法,這個方法需要保證和trigger的觸發邏輯一直。

2、使用mock數據(可行)

構造一個source,定時產生一個fake數據,這個數據只帶一個long類型的時間戳,這個時間戳要保證有一定的延遲。

這個流和另外兩個流分別做connect,把數據傳下去,一般來說,如果正常的流有數據,這個mock流產生的數據永遠是延遲很重會被丟棄的;如果某個流出現了數據沒來的情況,這個mock流的數據就不會被丟棄,可以用這個數據生成水印,從而觸發窗口計算和窗口清除。

3、使用connect,代替cogroup

cogroup有問題的原因就是它會緩存兩份數據,導致互相等待。

使用connect,直接糅合兩個流,用兩個流糅合後的流抽取時間戳和水印。然後在這個單流上做窗口,內部自己實現join邏輯。

4、將flink的狀態後端改爲rocksdb

當前flink的狀態後端是filesystem,平常的狀態數據會緩存在taskmanager裏,做checkpoint的時候會寫入文件系統;

改成rocksdb之後,狀態數據會序列化寫入rocksdb,做checkpoint的時候寫入文件系統。

這樣,就不會把內存撐爆了。

參考:https://www.jianshu.com/p/246b25cddc73

https://blog.csdn.net/lxhandlbb/article/details/90668040

啓動後觀察內存佔用

有時候不需要進行堆dump來查問題,可能只需要看一下線上的進程運行情況,這時候需要配合使用多種工具。

查看gc情況

# 這個是查看各個代的內存大小、ygc和fgc的次數以及耗時
$ jstat -gc pid

# 這個和上面差不多,只不過是將各個區的佔用情況用百分比表示,而不是絕對數量
jstat -gcutil pid

查看堆裏的對象佔用情況

# 查看內存中的所有對象信息,和上面的histogram展示的結果一樣,有類名、對象個數、對象大小、對象能直接和間接引用到的對象的總大小
$ jmap -histo pid

# 這個是先觸發一次full gc之後,再統計堆裏面的內存佔用情況
$ jmap -histo:live pid

查看堆裏各個區的大小和已經使用的大小

jmap -heap pid

查看進程的啓動信息,能看到jdk版本、jvm參數等信息

jinfo pid

 

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