背景
隨着汽車行業加速智能化轉型,從傳統線下 IDC 集羣向雲端遷移並進行容器化改造,經常會遇到關於 Pod 內存異常、Pod發生 OOMKilled 的問題, 這些問題主要的矛盾點在於:
1、Pod(容器)內存佔用比 JVM 內存監控(堆內和堆外內存)佔用大很多。
2、總是有一部分消失的內存無法找出具體是哪部分佔用。
3、同一業務同一 JDK 版本,切換 OS 或容器化改造之後,才出現了 1、2 現象。
雖然 Java 工具千千萬,但是選用什麼工具排查起這類 Java 內存問題也是一個頭疼的問題;甚至有時候翻遍了工具百寶箱,最後還是沒有排查出問題的根因。經歷過這些問題的洗禮之後,我們也從中總結了一些排查思路,並沉澱成一個阿里雲操作系統控制枱的 Java 內存診斷功能,幫助用户結合應用和操作系統的角度,快速揪出 Java 應用內存佔用的元兇。
消失的 Java 內存
Java 內存組成
為了找出消失的內存,我們首先要了解 Java 進程的主要內存組成以及現有工具和監控主要覆蓋的部分;如下圖所示可分為:
JVM 內存
- 堆內存:可通過 -Xms/-Xmx 參數控制,內存大小可通過 memorymxbean 等獲取。
- 堆外內存:包括元空間、壓縮類空間、代碼緩衝區、直接緩衝、線程棧等內存組成;它們分別可以通過-XX:MaxMetaspaceSize(元空間)、-XX:CompressedClassSpaceSize (壓縮類空間)、 -XX:ReservedCodeCacheSize( 代碼緩衝區)、-XX:MaxDirectMemorySize (直接緩衝)、-Xss(線程棧)參數限制。
非 JVM 內存
- JNI本地內存:即通過本地方法接口調用 C、C++ 代碼(原生庫),並在這部分代碼中調用 C 庫(malloc)或系統調用(brk、mmap)直接分配的內存。
常見 Java 內存黑洞
JNI 內存
通過對 Java 內存組成的瞭解,我們其實已經可以揭開第一個容易造成內存黑洞的隱藏 Boss-JNI 內存,因為這部分內存暫時沒有工具可以獲取其佔用大小。
通常來説,編寫相關業務代碼的同學會認為代碼中沒有使用本地方法直接調用 C 庫,所以不會存在這些問題,但是代碼中引用的各種包卻有可能會使用到 JNI 內存,比如説經典的使用 ZLIB 壓縮庫不當導致的 JNI 泄漏問題[2]。
LIBC 內存
熟悉 Java 的同學都知道,JVM 是由 C++ 編寫的,JVM 調用 malloc、free 申請/釋放內存的過程中其實還要經過一個二道販子 libc 庫;以 gibc 中默認的內存分配器 ptmalloc 為例:
chunk 是 glibc 堆內存分配的最小單位,表示一段連續的內存區域。ptmalloc 會為每一個線程維護一個 Arena,每一個 Arena 中會有一個大 chunk(Top chunk)和 chunk 的緩存集合(bins);當應用通過 malloc、free 申請/釋放內存時、ptmalloc 會優先從 bins 中拿取和釋放 chunk,如果沒有符合要求的 bins,再從 top chunk 裏面獲取、再沒有就通過 brk、mmap 系統調用從 OS 獲取。
從上面的流程,我們可以發現,libc 作為二道販子,很有可能多申請、扣留一些內存,從而導致 JVM 內存和進程實際內存的差異;我們也總結一下 libc 常見的一些問題:
- 多線程 64M Arena 內存佔用,libc 會為每個線程創建一個 64M 大小的 Arena,默認配置下在線程數量較多時會導致一定的內存浪費 [3]。
- Top chunk 由於內存空洞導致無法及時釋放回 OS [4]。
- bins 緩存,JVM 釋放的內存被緩存在 bins 中,導致內存差異 [4]。
透明大頁
到了 OS 層,Linux 中的透明大頁(Transparent Huge Page)機制也是造成 JVM 內存和實際內存差異的一大元兇。簡單來説,THP 機制就是 OS 會將 4kb 頁變成 2M 的大頁,從而減少 TLB miss 和缺頁中斷,提升應用性能,但是也帶來了一些內存浪費。如應用申請了一段 2M 的虛擬內存,但實際只用了裏面的 4kb,但是由於 THP 機制,OS 已經分配了一個 2M 的頁了[5]。
通過阿里雲操作系統控制枱揪出Java內存佔用元兇
操作系統控制枱是阿里雲推出的一站式運維管理平台,充分結合了阿里在百萬服務器運維領域的豐富經驗。集成了監控、系統診斷、持續追蹤、AI 可觀測、集羣健康度和 OS Copilot 等核心功能,專門應對雲端高負載、網絡延遲抖動、內存泄漏、內存溢出(OOM)、宕機、I/O 流量分析及性能抖動等各種複雜問題。
下面將以汽車行業客户在從線下 idc 集羣遷移至雲上 ACK 集羣時遇到的由於 JNI 內存泄漏導致 Pod 頻繁 OOM 為例,介紹如何通過操作系統控制枱的內存全景分析功能[5]來一步步找出 Java 內存佔用的元兇。
背景:
客户雲上多個集羣多個服務中的一些 Java 業務 pod 在沒有任何服務異常、請求異常、流量異常的跡象下會偶發 OOM,且從 jvm 監控上內存也沒有很大的波動(客户設置 5G limit,正常水位在 3G 左右)。
排查過程:
- 嘗試在內存高水位時對 Pod 發起內存全景分析,我們可以瞭解到當 Pod 中容器內存使用已經接近 limit,從診斷結論和容器內存佔用分析中,我們可以看到容器內存主要是由於 Java 進程內存佔用導致。
對 Java 進程發起內存分析,查看診斷報告。報告展示了 Java 進程所在 Pod 和容器的 rss 和 WorkingSet(工作集)內存信息、進程 Pid、JVM 內存使用量(即 JVM 視角的內存使用量)、Java 進程內存使用量(進程實際佔用內存),進程匿名用量以及進程文件內存用量。
通過診斷結論和 Java 內存佔用餅圖我們可以發現,進程實際內存佔用比 JVM 監控顯示的內存佔用大 570M,全都由 JNI 內存所貢獻。
開啓 JNI(Java Native Interface)內存分配 profiling,報告列出當前 Java 進程 JNI 內存分配調用火焰圖,火焰圖中為所有分配 JNI 內存的調用路徑。(説明:由於是採樣採集,火焰圖中的內存大小不代表實際分配大小)。
- 從內存分配火焰圖中,我們可以看到主要的內存申請為 C2 compiler 正在進行代碼 JIT 預熱;
- 但是由於診斷的過程中沒有發現 pod 有內存突增;所以我們進一步藉助可以常態化運行的 Java CPU 熱點追蹤功能[7]嘗試抓取內存升高時的進程熱點,並通過熱點對比[8]嘗試對內存正常時的熱點進行對比。
- 通過熱點棧和熱點分析對比,發現內存突增時間點的 CPU 棧也是 c2 compiler 的JIT 棧,且 c2 compiler 熱點前有部分業務流量突增,且業務代碼使用了大量反射操作(反射操作會導致 c2 compiler 進行新的預熱)。
排查結論:
C2 compiler JIT 過程申請 JNI 內存,且由於 glibc 內存空洞等原因導致申請內存放大且延時釋放。
緩解方法:
1.調整 C2 compiler 參數,讓其編譯策略更保守,可以嘗試調整相關參數,觀察內存消耗變化。
2.調整 glibc 環境變量 MALLOC_TRIM_THRESHOLD_,讓 glibc 及時將內存釋放回操作系統。
聯繫我們
您在使用操作系統控制枱的過程中,有任何疑問和建議,可以搜索羣號:94405014449 加入釘釘羣反饋,歡迎大家掃碼加入交流。
相關鏈接:
【1】阿里雲操作系統控制枱PC端鏈接:https://alinux.console.aliyun.com/
【2】java.util.zip內存泄露:https://bugs.openjdk.org/browse/JDK-8257032
【3】glibc 64M arena內存浪費:https://bugs.openjdk.org/browse/JDK-8193521
【4】glibc top chunk/fast bin內存不回收:https://wenfh2020.com/2021/04/08/glibc-memory-leak/#332-fast-...
【5】go應用由於thp導致內存膨脹:https://github.com/golang/go/issues/64332
【6】操作系統控制枱內存全景分析:https://help.aliyun.com/zh/alinux/user-guide/memory-panorama-...
【7】操作系統控制枱熱點追蹤:https://help.aliyun.com/zh/alinux/user-guide/process-hotspot-...
【8】操作系統控制枱熱點對比分析:https://help.aliyun.com/zh/alinux/user-guide/hot-spot-compara...