每天億級數據量的日志清洗系統,從Kafka不停消費各種日志數據,然后對日志格式進行各種清洗,如對敏感信息字段(姓名、手機號、身份證號)進行脫敏,再把清洗后數據給其它系統,如推薦系統、廣告系統、分析系統都會去使用這些清洗好的數據。
OOM 現場
某天我們也是突然收到線上的報警,發現日志清洗系統發生OOM。第一時間登錄到線上機器,查看日志,經典日志
JAVA.lang.OutOfMemoryError: java heap space
堆內存溢出。套路應該很熟了。
先看異常日志,定位啥導致的問題,日志里看到了:
java.lang.OutOfMemoryError: java heap space xx.xx.xx.log.clean.XXClass.process() xx.xx.xx.log.clean.XXClass.xx() xx.xx.xx.log.clean.XXClass.xx() xx.xx.xx.log.clean.XXClass.process() xx.xx.xx.log.clean.XXClass.xx() xx.xx.xx.log.clean.XXClass.xx() xx.xx.xx.log.clean.XXClass.process() xx.xx.xx.log.clean.XXClass.xx() xx.xx.xx.log.clean.XXClass.xx()
似乎同樣的一個方法(XXClass.process())反復出現,最終導致堆內存溢出。
再仔細分析日志,代碼出現大量遞歸操作,正是這大量遞歸,導致堆內存溢出。接著自然就是MAT分析內存快照。
初步分析內存快照
雖然日志能看到哪個方法導致內存溢出,但不知道到底哪個方法調用創建的大量對象。因此還得用MAT分析。
因大量XXClass.process()遞歸執行,每個XXClass.process()中都創建了大量char數組,最后因XXClass.process()又多次遞歸調用,也就導致大量的char[]數組耗盡內存:
JVM參數問題?
基本定位出問題了,但先別急直接去代碼檢查問題,因為發現了比較大問題。
雖然XXClass.process()遞歸調用多次,但實際上我們在MAT中發現遞歸調用的次數也不多,最多幾十次。所有遞歸調用加起來創建的char[]數組對象總和其實也最多1G。
可能這次OOM發生不一定是代碼寫的爛,可能就是JVM 內存參數不對,給堆內存分配太少。
分析GC日志
要知道堆內存是不是設小了,就得分析JVM運行時的內存使用模型。
現在宕機了,唯一能看到的,就是當時在JVM啟動參數中加入的自動記錄的GC日志。
從GC日志中,可見JVM啟動時的完整參數設置:
-Xmx1024m
-Xms1024m
-XX:+PrintGCDetails
-XX:+PrintGC()
-XX:+HeapDumpOnOutOfMemoryError
-Xloggc:/opt/logs/gc.log
-XX:HeapDumpPath=/opt/logs/dump
這里主要是把gc日志詳細記錄在了一個日志文件,另外指定OOM時要導出內存快照,堆內存1G,但這臺機器可是4核8G!
記錄下來的gc.log日志:
[Full GC (Allocation Failure) 866M->654M(1024M)]
[Full GC (Allocation Failure) 843M->633M(1024M)]
[Full GC (Allocation Failure) 855M->621M(1024M)]
[Full GC (Allocation Failure) 878M->612M(1024M)]
因Allocation Failure觸發的Full GC很多,即堆內存無法創建新對象,然后觸發GC,結果觸發時肯定先觸發Full GC了。發現每次Full GC都只回收一點點對象,堆內存幾乎都占滿的。
日志顯示的是每s都會執行一次Full GC,這就絕了。應該是在系統運行時,因為XXClass.process()不停遞歸創建大量char[]數組,導致堆內存幾乎滿。
然后導致連續一段時間,每s觸發一次Full GC,因為內存都滿了,特別是Old可能幾乎都滿了,所以可能每s執行YGC前,發現Old可用空間不足,就會提前觸發Full GC。
也可能YGC后,存活對象太多無法放入Survivor,而都要進入Old,也放不下了,只能Full GC。但每次full gc只能回收少量對象,直到最后可能某次full gc回收不掉任何對象了,然后新的對象無法放入堆內存了,就觸發OOM了:
明顯就是堆內存偏小了,導致頻繁full gc。
分析一下JVM運行時內存使用模型
接著jstat分析當時JVM運行時的內存模型,當時重啟系統,每s打印一次:
S0 S1 E O YGC FGC
0 100 57 69 36 0
0 100 57 69 36 0
0 100 65 69 37 0
0 100 0 99 37 0
0 100 0 87 37 1
我就給出部分信息大家就可以看出來問題所在了,剛開始都是Eden在漲,接著YGC從36到37,發生了一次YGC,Old直接從69%到99%。
why?
YGC后存活對象太多,Survivor放不下,直接進了Old!接著Old都占99%了,直接觸發一次Full GC,但也僅讓Old從99%到87%,只回收少量對象。
該過程反復循環了幾次,年輕代對象反復進入老年代,不停觸發Full GC,但是還回收不了多少對象,幾次循環過后,老年代滿了,可能Full GC沒回收多少對象,新的對象一大批放不下了,就觸發OOM了。
優化第一步:加大堆內存
所以這OOM,不能一口說是代碼問題,從JVM運行情況及內存大小來看,其實是內存分配不足問題。
第一步,直接在4核8G的機器上,加大堆內存,直接給堆5G。
接著運行系統,jstat發現,每次YGC過后存活對象都落入Survivor區域了,不會隨便進入老年代,而且因為堆內存很大,基本上運行一段時間不會發生OOM問題了。
優化第二步:改寫代碼
讓他不要占用過多內存。代碼之所以遞歸,是因為在一條日志中,可能出現很多用戶的信息,一條日志也許會合并包含了十幾個到幾十個用戶的信息。
這時代碼中就是會遞歸處理日志,每次遞歸都會產生大量char[]數組,是切割了日志用來處理的。
這代碼寫的完全沒必要,因為對每一條日志,若發現包含多個用戶的信息,就對這一條日志切割出來進行處理,沒必要遞歸調用,每次調用都切割一次日志,生成大量char[]數組。
該步代碼優化后,線上系統的內存使用情況降低10倍以上。
總結
先通過OOM的排查方法去分析,發現主要是內存太小導致的問題然后用gc日志和jstat分析,明顯發現是內存不夠用了,最后加大系統內存,并優化代碼即可。