本文原載于Elastic中文社區(qū): https://elasticsearch.cn/publish/article/178
近期公司某個(gè)線上JAVA應(yīng)用出現(xiàn)內(nèi)存泄漏問題绰疤,整個(gè)排查過程頗費(fèi)周折脂信,前后耗費(fèi)了近2周才定位到問題根源并予以修復(fù)。排查問題過程中在網(wǎng)上翻查了大量的資料,發(fā)現(xiàn)國內(nèi)幾乎沒有文章能對(duì)同類問題做透徹的分析和找到問題真正根源的儒恋。即使國外的各類博客和文章湖笨,也少有正確的分析。因此感覺有必要對(duì)問題根源和相關(guān)案例做一個(gè)總結(jié)灵寺,希望能為國內(nèi)開發(fā)者避免踩上同類陷阱提供一些幫助曼库。
開門見山,先列一下收集到的同類問題案例集:
- Debugging Java Native Memory Leaks
- Tracking Down Native Memory Leaks in Elasticsearch
- CompressingStoredFieldsFormat should reclaim memory more aggressively
- Close InputStream when receiving cluster state in PublishClusterStateAction
- Kafka OOM During Log Recovery Due to Leaked Native Memory
這些案例涉及到的不乏一些流行的開源軟件如Lucene, Elasticsearch, Kafka略板,并且某些Bug版本在大量公司有線上部署毁枯。這些案例的問題根源都驚人的一致,即在JAVA里使用GZIP庫進(jìn)行數(shù)據(jù)流的壓縮/解壓之后叮称,忘記調(diào)用流的close()方法种玛,從而造成native memory的泄漏。
關(guān)于這類問題的分析方法和工具瓤檐,上面收集的案例集里有非常詳盡的描述赂韵,這里就不再班門弄斧一一贅述了。只結(jié)合我們自己的案例距帅,做一個(gè)比較簡(jiǎn)短的介紹和總結(jié)右锨。
我們公司這個(gè)案例的排查之所以花了近2個(gè)禮拜,其中一個(gè)重要原因是這個(gè)應(yīng)用是通過Docker部署的碌秸。應(yīng)用上線運(yùn)行一段時(shí)間后绍移,會(huì)被Docker的OOM killer給Kill掉,查看JVM監(jiān)控?cái)?shù)據(jù)卻發(fā)現(xiàn)Heap使用得很少讥电,甚至都沒有old GC發(fā)生過蹂窖,top里看這個(gè)JAVA進(jìn)程的RSS內(nèi)存占用遠(yuǎn)高于分配的Heap大小。很自然的恩敌,研發(fā)人員第一反應(yīng)是底層系統(tǒng)的問題瞬测,注意力被轉(zhuǎn)移到研究各種Docker內(nèi)存相關(guān)的參數(shù)上。 而我知道ElasticCloud曾經(jīng)也被某些版本的linux內(nèi)核bug困擾,docker可能會(huì)誤殺JVM (參見Memory Issues We'll Remember)月趟,bug的內(nèi)核版本和docker版本和我們線上部署的又很接近灯蝴,因此這個(gè)內(nèi)核bug也被加入到了懷疑列表中。 事后證明這個(gè)方向是錯(cuò)誤的孝宗,浪費(fèi)了一些時(shí)間穷躁。
在一段時(shí)間排查無果后,為了縮小排查范圍因妇,我們決定將這個(gè)應(yīng)用部署到VM上做對(duì)比測(cè)試问潭。結(jié)果內(nèi)存泄漏問題依然存在,因而排除掉了Linux內(nèi)核和docker本身的問題婚被。
同期也參考過一篇關(guān)于DirectByteByffer造成堆外內(nèi)存泄漏問題的分析博客狡忙,JVM源碼分析之堆外內(nèi)存完全解讀,考慮到問題現(xiàn)象和我們類似址芯,我們的應(yīng)用也有用到netty灾茁,DBB泄漏也被列為懷疑對(duì)象。然而在JVM啟動(dòng)里參數(shù)里對(duì)MaxDirectMemorySize做了限制后是复,經(jīng)過一段時(shí)間對(duì)外服務(wù)删顶,JAVA進(jìn)程的RSS仍然會(huì)遠(yuǎn)超過HEAP + MDM設(shè)置的大小。
這期間我們也使用過NMT工具分析HEAP內(nèi)存占用情況淑廊,然而這個(gè)工具報(bào)告出來的內(nèi)存遠(yuǎn)小于RSS逗余,也就是說這多出來的內(nèi)存并沒有被JVM本身用到,泄漏的是native memory季惩。 JAVA應(yīng)用產(chǎn)生native memory泄漏通常是在使用某些native庫時(shí)造成的录粱,因此注意力轉(zhuǎn)移到JNI。
最終幫助我們找到正確方向的是開頭列的 Debugging Java Native Memory Leaks 這篇由Twitter工程師寫的博客画拾。 博客里介紹了如何使用jemalloc來替換glibc的malloc啥繁,通過攔截和追蹤JVM對(duì)native memory的分配申請(qǐng),從而可以分析出HEAP以外的內(nèi)存分配由哪些方法調(diào)用產(chǎn)生的青抛。博客里提到產(chǎn)生泄漏的原因是忘記關(guān)閉GZIPOutputStream旗闽,巧合的是我們線上應(yīng)用也使用了gzip壓縮服務(wù)請(qǐng)求數(shù)據(jù),于是查看了一下相關(guān)的代碼蜜另,果然發(fā)現(xiàn)有忘記關(guān)閉的stream适室。 找到根源后,解決問題就簡(jiǎn)單了举瑰,一行代碼修復(fù)捣辆。
對(duì)于ElasticSearch用戶,要注意的是某些版本存在這個(gè)泄漏問題此迅,對(duì)于小內(nèi)存機(jī)器上運(yùn)行的ES服務(wù)可能會(huì)有較大的影響汽畴。 可是官方?jīng)]有明確列出所有受影響的版本旧巾,只在博客里提到5.2.1修復(fù)了這些問題。 因此如果你有顧慮的話忍些,可以用top命令看一下ES JAVA進(jìn)程的RSS消耗鲁猩,如果大大于分配的HEAP,有可能就是中招啦坐昙。