阿里妹導讀
本文記錄一次glibc導致的堆外內存泄露的排查過程。
問題現象
團隊核心應用每次發布完之后,內存會逐步占用,不重啟或者重新部署就會導致整體內存占用率超過90%。
發布2天后的內存占用趨勢
探索原因一
堆內找到原因
出現這種問題,第一想到的就是集群中隨意找一臺機器,信手dump一下內存,看看是否有堆內存使用率過高的情況。 內存泄露 泄露對象占比 發現 占比18.8%
問題解決
是common-division這個包引入的
暫時性修復方案
當前加載俄羅斯(RU)國際地址庫,改為一個小國家地址庫 以色列(IL)
當前業務使用場景在補發場景下會使用,添加打點日志,確保是否還有業務在使用該服務,沒人在用的話,直接下掉(后發現,確還有業務在用呢 )。
完美解決問題,要的就是速度!!發布 ~~上線!!順道記錄下同一臺機器的前后對比。
發布后短時間內有個內存增長實屬正常,后續在做觀察。
發布第二天,順手又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 aresident(until it directly accessed).
rssAnalyzer 內存分析
筆者沒有使用,因為本功能與NMT作用類似,暫時沒有截圖了~ rssAnalyzer(內部工具),可以通過oss在預發/線上下載。
通過NMT查看內存使用,基本確認是堆外內存泄露。 剩下的分析過程就是確認是否堆外泄露,哪里在泄露。
堆外內存分析
查了一堆文檔基本思路就是
pmap 查看內存地址/大小分配情況
確認當前JVM使用的內存管理庫是哪種
分析是什么地方在用堆外內存。
內存地址/大小分配情況
pmap 查看
pmap -x 2531 | sort -k 3 -n -r
劇透: 32位系統中的話,多為1M 64位系統中,多為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
核心配置
make之后,需要啟用prof,否則會出現『
配置環境變量
LD_PRELOAD 掛載本次編譯的庫
MALLOC_CONF 配置dump內存的時機。
"lg_prof_sample:N",平均每分配出 2^N 個字節 采一次樣。當 N = 0 時,意味著每次分配都采樣。
"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
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的內存使用上確實存在高碎片率的問題,但是本次問題的根本還是在應用層面沒有正確的關閉流加劇的堆外內存的泄露。
總結的過程,也是學習的過程,上述分析過程歡迎評論探討。
審核編輯:湯梓紅
-
內存
+關注
關注
8文章
3055瀏覽量
74329 -
Glibc
+關注
關注
0文章
9瀏覽量
7525 -
內存泄露
+關注
關注
0文章
6瀏覽量
2003
原文標題:實戰總結|記一次glibc導致的堆外內存泄露
文章出處:【微信號:OSC開源社區,微信公眾號:OSC開源社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論