實戰總結|記一次 glibc 導致的堆外內存泄露

問題現象 

團隊核心應用每次發佈完之後,內存會逐步佔用,不重啓或者重新部署就會導致整體內存佔用率超過90%。

發佈2天后的內存佔用趨勢

探索原因一

堆內找到原因

出現這種問題,第一想到的就是集羣中隨意找一臺機器,信手dump一下內存,看看是否有堆內存使用率過高的情況。

內存泄露

泄露對象佔比

發現 佔比18.8%

問題解決

是common-division這個包引入的

暫時性修復方案

  1. 當前加載俄羅斯(RU)國際地址庫,改爲一個小國家地址庫 以色列(IL)
  2. 當前業務使用場景在補發場景下會使用,添加打點日誌,確保是否還有業務在使用該服務,沒人在用的話,直接下掉(後發現,確還有業務在用呢 )。

完美解決問題,要的就是速度!!發佈 ~~上線!!順道記錄下同一臺機器的前後對比。

發佈後短時間內有個內存增長實屬正常,後續在做觀察。

發佈第二天,順手又dump一下同一臺機器的內存

由原來的18.8%4.07%的佔比,降低了14%,牛皮!!

傻了眼,內存又飆升到86%~~ 該死的迷之自信!!

發佈後內存使用率

探索原因二

沒辦法彙報了~~~但是問題還是要去看看爲什麼會佔用這麼大的內存空間的~

查看進程內存使用

java 進程內存使用率 84.9%,RES 6.8G。

查看堆內使用情況

當期機器配置爲 4Core 8G,堆最大5G,堆使用爲不足3G左右。

使用arthas的dashboard/memory 命令查看當前內存使用情況:

當前堆內+非堆內存加起來,遠不足當前RES的使用量。那麼是什麼地方在佔用內存??

開始初步懷疑是『堆外內存泄露』

開啓NMT查看內存使用

筆者是預發環境, 正式環境開啓需謹慎,本功能有5%-10%的性能損失!!!
-XX:NativeMemoryTracking=detail

jcmd pid VM.native_memory

如圖有很多內存是Unknown(因爲是預發開啓,相對佔比仍是很高)。

概念
NMT displays  “committed” memory, not "resident" (which you get through the ps command). In other words, a memory page can be committed without considering as a  resident (until it directly accessed).

rssAnalyzer 內存分析

筆者沒有使用,因爲本功能與NMT作用類似,暫時沒有截圖了~
rssAnalyzer(內部工具),可以通過oss在預發/線上下載。

通過NMT查看內存使用,基本確認是堆外內存泄露。剩下的分析過程就是確認是否堆外泄露,哪裏在泄露。

堆外內存分析

查了一堆文檔基本思路就是

  1. pmap 查看內存地址/大小分配情況
  2. 確認當前JVM使用的內存管理庫是哪種
  3. 分析是什麼地方在用堆外內存。

內存地址/大小分配情況

pmap 查看

pmap -x 2531 | sort -k 3 -n -r

劇透:
32位系統中的話,多爲1M64位系統中,多爲64M。

strace 追蹤

由於系統對內存的申請/釋放是很頻繁的過程,使用strace的時候,無法阻塞到自己想要查看的條目,推薦使用pmap。

strace -f -e"brk,mmap,munmap" -p 2853
原因: 對 heap 的操作,操 作系統提供了 brk()函數,C 運行時庫提供了 sbrk()函數;對 mmap 映射區域的操作,操作系 統提供了 mmap()和 munmap()函數。sbrk(),brk() 或者 mmap() 都可以用來向我們的進程添 加額外的虛擬內存。Glibc 同樣是使用這些函數向操作系統申請虛擬內存。

查看JVM使用內存分配器類型

發現很大量爲[anon](匿名地址)的64M內存空間被申請。通過附錄參考的一些文檔發現很多都提到64M的內存空間問題(glibc內存分配器導致的),抱着試試看的態度,準備看看是否爲glibc。

cd /opt/taobao/java/bin
ldd java

glibc爲什麼會有泄露

我們當前使用的glibc的版本爲2.17。說到這裏可能簡單需要介紹一下glibc的發展史。

V1.0時代』Doug Lea Malloc 在Linux實現,但是在多線程中,存在多線程競爭同一個分配分配區(arena)的阻塞問題。
V2.0時代』Wolfram Gloger 在 Doug Lea 的基礎上改進使得 Glibc 的 malloc 可以 支持多線程——ptmalloc。

glibc內存釋放機制(可能出現泄露時機)

調用free()時空閒內存塊可能放入 pool 中,不一定歸還給操作系統。
.收縮堆的條件是當前 free 的塊大小加上前後能合併 chunk 的大小大於 64KB、,並且 堆頂的大小達到閾值,纔有可能收縮堆,把堆最頂端的空閒內存返回給操作系統。
『V2.0』爲了支持多線程,多個線程可以從同一個分配區(arena)中分配內存,ptmalloc 假設線程 A 釋放掉一塊內存後,線程 B 會申請類似大小的內存,但是 A 釋放的內 存跟 B 需要的內存不一定完全相等,可能有一個小的誤差,就需要不停地對內存塊 作切割和合並。

爲什麼是64M

回到前面說的問題,爲什麼會創建這麼多的64M的內存區域。這個跟glibc的內存分配器有關下的,間作介紹。

V2.0版本的glibc內存分配器,將分配區域分配主分配區(main arena)和非主分配區(non main arena)(在v1.0時代,只有一個主分配區,每次進行分配的時候,需要對主分配區進行加鎖,2.0支持了多線程,將分配區通過環形鏈表的方式進行管理),每一個分配區利用互斥鎖使線程對於該分配區的訪問互斥。

主分配區:可以通過sbrk/mmap進行分配。
非主分配區,只可以通過mmap進行分配。
其中,mmap每次申請內存的大小爲HEAP_MAX_SIZE(32 位系統上默認爲 1MB,64 位系統默 認爲 64MB)。

哪裏在泄露

既然知道了存在堆外內存泄露,就要查一下到低是什麼地方的內存泄露。參考歷史資料,可以使用jemalloc工具進行排查。

配置dump內存工具(jemalloc)

由於系統裝載的是glibc,所以可以自己在不升級jdk的情況下編譯一個jemalloc。

github下載比較慢,上傳到oss,再做下載。
sudo yum install -y git gcc make graphviz 
    wget -P /home/admin/general-aftersales https://xxxx.oss-cn-zhangjiakou.aliyuncs.com/jemalloc-5.3.0.tar.bz2 &&  \
    mkdir   /home/admin/general-aftersales/jemalloc && \
    cd  /home/admin/general-aftersales/ && \
    tar -jxcf  jemalloc-5.3.0.tar.bz2 && \
    cd  /home/admin/xxxxx/jemalloc-5.3.0/ && \
    ./configure  --enable-prof && \
    make && \
    sudo make install

export LD_PRELOAD=/usr/local/lib/libjemalloc.so.2  MALLOC_CONF="prof:true,lg_prof_interval:30,lg_prof_sample:17,prof_prefix:/home/admin/general-aftersales/prof_prefix

核心配置

  1. make之後,需要啓用prof,否則會出現『<jemalloc>: Invalid conf pair: prof:true』類似的關鍵字
  2. 配置環境變量
    1. LD_PRELOAD 掛載本次編譯的庫
    2. MALLOC_CONF 配置dump內存的時機。
    3. "lg_prof_sample:N",平均每分配出 2^N 個字節 採一次樣。當 N = 0 時,意味着每次分配都採樣。
    4. "lg_prof_interval:N",分配活動中,每流轉 1 « 2^N 個字節,將採樣統計數據轉儲到文件。

重啓應用

./appctl restart

監控內存dump文件

如果上述配置成功,會在自己配置的prof_prefix 目錄中生成相應的dump文件。

然後將文件轉換爲svg格式

jeprof --svg /opt/taobao/java/bin/java prof_prefix.36090.9.i9.heap > 36090.svg

然後就可以在瀏覽器中瀏覽了

與參閱文檔中結果一致,有通過 Java java.util.zip.Inflater 調用JNI申請內存,進而導致了內存泄露。

既然找到了哪裏存在內存泄露,找到使用的地方就很簡單了。

發現元兇

通過arthas 的stack命令查看某個方法的調用棧。

statck java.util.zip.Inflater <init>

java.util.zip.InflaterInputStream

 

如上源碼可以看出,如果使用InflaterInputStream(InputStream in) 來構造對象usesDefaultInflater=true, 否則全部爲false;

在流關閉的時候。

 
end()是native方法。

只有在『usesDefaultInflater=true』的時候,纔會調用free()將內存歸嘗試歸還OS,依據上面的內存釋放機制,可能不會歸還,進而導致內存泄露。

comp.taobao.pandora.loader.jar.ZipInflaterInputStream

二方包掃描

ZipInflaterInputStream 的流關閉使用的是父類java.util.zip.InflaterInputStream,構造器使用public InflaterInputStream(InputStream in, Inflater inf, int size)

這樣如上『usesDefaultInflater=false』,在關閉流的時候,不會調用end()方法,導致內存泄露。

com.taobao.pandora.loader.jar.ZipInflaterInputStream 源自pandora ,諮詢了相關負責人之後,發現2年前就已經修復此內存泄露問題了。

最低版本要求
sar包裏的 pandora 版本,要大於等於 2.1.17

問題解決

升級ajdk版本

需要諮詢一下jdk團隊的同學,需要使用jemalloc作爲內存分配器的版本。

升級pandora版本

如上所說,版本高於2.1.17即可。

我們是團隊是統一做的基礎鏡像,找相關的同學做了dockerfile from的升級。

發佈部署&觀察

 

這此真的舒服了~

總結

探究了glibc的工作原理之後,發現相比jemalloc的內存使用上確實存在高碎片率的問題,但是本次問題的根本還是在應用層面沒有正確的關閉流加劇的堆外內存的泄露。

總結的過程,也是學習的過程,上述分析過程歡迎評論探討。

作者|叔耀

點擊立即免費試用雲產品 開啓雲上實踐之旅!

原文鏈接

本文爲阿里雲原創內容,未經允許不得轉載

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