記一次堆外記憶體洩漏排查過程

MartinDai發表於2024-06-10

本文涉及以下內容

  • 開啟NMT檢視JVM記憶體使用情況
  • 透過pmap命令檢視程序實體記憶體使用情況
  • smaps檢視程序記憶體地址
  • gdb命令dump記憶體塊

背景

最近收到運維反饋,說有專案的一個節點的RSS已經是Xmx的兩倍多了,因為是ECS機器所以專案可以一直執行,幸虧機器記憶體充足,不然就可能影響到其他應用了。

排查問題

透過跳板機登入到目標機器,執行top命令,再按c,看到對應的程序所佔用的RES有8個多G(這裡當時忘記截圖了),但是實際上我們配置的Xmx只有3G,而且程式還是正常執行的,所以不會是堆佔用了這麼多,於是就把問題方向指向了非堆的記憶體。

首先想到透過Arthas檢視記憶體分佈情況,執行dashboard命令,檢視記憶體分佈情況

1

發現堆和非堆的記憶體加起來也就2個G不到,但是這裡看到的非堆只包含了code_cache和metaspace的部分,那有沒有可能是其他非堆的部分有問題呢?

NMT

NMT是Native Memory Tracking的縮寫,是Java7U40引入的HotSpot新特性,開啟後可以透過jcmd命令來對JVM記憶體使用情況進行跟蹤。注意,根據Java官方文件,開啟NMT會有5%-10%的效能損耗。

-XX:NativeMemoryTracking=[off | summary | detail]  
# off: 預設關閉 
# summary: 只統計各個分類的記憶體使用情況.
# detail: Collect memory usage by individual call sites.

新增-XX:NativeMemoryTracking=detail命令到啟動引數中,然後重啟專案

跑了一段時間後top看了下程序的RES,發現已經5個多G了

執行jcmd <pid> VM.native_memory summary scale=MB

2

從圖中可以看到堆和非堆的總使用記憶體(committed)也就2G多,那還有3個G的記憶體去哪裡了呢

pmap

pmap命令是Linux上用來開程序地址空間的

執行pmap -x <pid> | sort -n -k3 > pmap-sorted.txt命令可以根據實際記憶體排序

檢視pmap-sorted.txt檔案,發現有大量的64M記憶體塊

3

難道是linux glibc 中經典的 64M 記憶體問題?之前看挖坑的張師傅寫過一篇文章(一次 Java 程序 OOM 的排查分析(glibc 篇))講過這個問題,於是準備參考一下排查思路

嘗試設定環境變數MALLOC_ARENA_MAX=1,重啟專案,跑了一段時間以後,再次執行pmap命令檢視記憶體情況,發現並沒有什麼變化,看來並不是這個原因,文章後面的步驟就沒有什麼參考意義了。

smaps + gdb

既然可以看到有這麼多異常的記憶體塊,那有沒有辦法知道里面存的是什麼內容呢,答案是肯定的。經過一番資料查閱,發現可以透過gdb的命令把指定地址範圍的記憶體塊dump出來。

要執行gdb的dump需要先知道一個地址範圍,透過smaps可以輸出程序使用的記憶體塊詳細資訊,包括地址範圍和來源

cat /proc/<pid>/smaps > smaps.txt

檢視smaps.txt,找到有問題的記憶體塊地址,比如下圖中的 7fb9b0000000-7fb9b3ffe000

4

啟動gdb

gdb attach <pid>

dump指定記憶體地址到指定的目錄下,引數的地址需要在smaps拿到地址前加上0x

dump memory /tmp/0x7fb9b0000000-0x7fb9b3ffe000.dump 0x7fb9b0000000 0x7fb9b3ffe000

顯示長度超過10字元的字串

strings -10 /tmp/0x7fb9b0000000-0x7fb9b3ffe000.dump

5

發現裡面有大量的圖中紅框中的內容,這個內容是後端給前端websocket推送的內容,怎麼會駐留在堆外記憶體裡面呢?檢查了專案程式碼發現,後端的websocket是使用的netty-socketio實現的,maven依賴為

<dependency>
 <groupId>com.corundumstudio.socketio</groupId>
 <artifactId>netty-socketio</artifactId>
 <version>1.7.12</version>
</dependency>

這是一個開源的socket.io的一個java實現框架,具體可以看

https://github.com/mrniko/netty-socketio

看了下最近的版本釋出日誌,發現這個框架的最新版本已經是1.7.18,而且中間釋出的幾個版本多次修復了記憶體洩漏相關的問題

6

於是把依賴版本升級到最新版,重新發布後,第二天在看,發現RES還是變得很高

中間又仔細看了使用框架的相關程式碼,發現斷開連線的時候沒有呼叫leaveRoom方法,而是呼叫了joinRoom,導致群發資訊的時候還會往斷開的連線裡面傳送資料(理論上會報錯,但是沒有看到過相關日誌),修復程式碼後又重新發布了一版,跑了一天在看,依然沒有效果。

結論

到這一步可以確認的是記憶體洩漏的問題肯定跟這個websocket及相關功能有關的(因為中間升級到1.7.18發現websocket連不上了,後來降到了1.7.17,期間有較長的時間是沒有暴露websocket服務的,而正好這段時間的RES是非常穩定的,完全沒有上漲的趨勢,因為這個功能點比較小,使用者沒有什麼感知),最後,經過跟產品方面溝通,決定把這裡的websocet去掉,改為前端直接請求介面獲取資料,因為這裡的功能就是為了實時推送一個未讀資訊數量,而這個資訊其實很少有使用者關心,所以,就改成重新整理頁面的時候查詢一下就行了,就這樣,問題算是變相解決了😁。至於這個問題的具體原因還是沒有找到,可能是框架BUG,也可能是程式碼使用問題,後面需要重度依賴websocket的時候或許會基於netty自己寫一套,這樣比較可控一點。

總結

雖然經過這麼多的努力,最終只是證明了【沒有需求就沒有BUG】這句話,但是中間還是有挺多的收穫的,很多命令也是第一次使用,中間還有一些曲折,就沒有一一寫出來了,挑選了一些比較有價值的過程寫了這篇文章總結,希望可以分享給有需要的人,以後遇到類似問題,可以做個經驗參考。

感謝

這次排查問題從網上查詢學習了很多資料,非常感謝以下文章作者分享的經驗

  • 一次 Java 程序 OOM 的排查分析(glibc 篇)
  • JAVA堆外記憶體排查小結
  • Linux中使用gdb dump記憶體

相關文章