解決內存泄漏(1)-Apache Kylin InternalThreadLocalMap泄漏問題分析

開源產品迭代快,但也容易存在隱患。有時會遇到意料之外的問題,需要研究代碼解決。內存泄漏是一個很常見的問題,會導致服務不穩定,影響可用性。本文講述瞭如何使用MAT和BTrace解決apache kylin內存泄漏問題,重點闡明如何定位問題,分析原因,驗證猜想。

希望能拋磚引玉,讓大家遇到類似內存泄漏問題時能夠有所借鑑。

背景

公司自助報表業務從kylin2.0集羣遷移到Kylin3.0集羣時,Kylin job角色每隔2,3天所有進程OOM一遍,服務很不穩定,需要儘快解決。

調查思路

構建服務是32GB堆內存的java進程,OOM要麼是內存確實不夠用,要麼是內存泄漏。考慮到報表業務之前使用的kylin2.0也是32GB內存,沒遇到類似的OOM,所以首先懷疑內存泄漏,可能是2.0以後的新特性引入了問題。

再考慮到kylin3.0小集羣用了很久也沒有OOM,懷疑跟業務量和用法有關係。報表業務每天要構建幾千次,構建模型各異,容易暴露出問題。一般來說,容器,Netty,ThreadLocal是內存泄漏的重災區。要調查內存泄漏,可以使用內存分析工具MAT來分析堆內存。找到哪些對象內存用的多,以及對象的引用關係。找到懷疑的對象後,再做進一步的代碼分析,用BTrace打印調用日誌確定問題的原因,最後解決問題。

定位問題

java啓動參數一般都會加-XX:+HeapDumpOnOutOfMemoryError,OOM時可以dump堆內存,生成java_pidxxxx.hprof文件供我們分析。

可以使用MAT(Memory Analyzer Tool)分析hprof文件。MAT的使用可自行谷歌,有需要可以給我留言。要注意的是要堆內存有幾十GB,需要幾十GB的空閒內存做分析。一般把MAT部署在內存空閒的測試服務器上,用vncserver提供可視化操作支持。

拷貝hprof文件到MAT服務器,啓動MAT,加載hprof文件。

加載時如果報錯

An internal error occurred during: 
"Parsing heap dump from **\java_pid6564.hprof'".Java heap space

需要增加MAT的啓動內存,至少比hprof文件大

open the MemoryAnalyzer.ini file
change the default -Xmx1024m to a larger size

MAT分析結果

上圖可以看出,佔用內存較多的對象是調度線程或其他線程的線程對象,每個線程對象佔用了較大內存。每次dump,線程對象大小不一,從200MB到1GB都有。但同一次dump,每個線程對象的大小几乎是一樣的。線程中主要是InternalThreadLocalMap對象佔用了內存,其中成員變量Object[]數組的length有幾千萬甚至上億,並且每個線程的數組長度都是一樣。這十有八九是內存泄漏。

那麼InternalThreadLocalMap是什麼對象呢?

分析原因

git blame,發現InternalThreadLocalMap 是KYLIN-3716 引入的,從jira看是借鑑了netty的相關代碼,將ThreadLocal的引用替換爲InternalThreadLocal(netty叫FastThreadLocal),從而讓查詢請求內部加載上下文的速度更快。那麼加快的原理又是什麼?

ThreadLocal在線程內部用map維護了線程內各個本地對象的引用,使用時通過查map定位到具體的對象。

而InternalThreadLocal在線程內部維護了本地對象數組,使用時查數組。對java來說,用數組索引查對象要比查map快。從實現來看,每次構建InternalThreadLocal對象,Ojbect[]的的有效索引位會加1,來緩存本地線程內對應的對象引用。但索引位只有加沒有減,如果InternalThreadLocal對象構建的特別多,Object[]的length就會很大,就會有內存問題。

一般來說,ThreadLocal對象都是當做靜態成員變量使用的,一個進程有幾十個對象就很多了,替換成InternalThreadLocal,數組也不會很大,不會有問題。但如果不加控制,當成普通的成員變量來用,就會有內存泄漏的風險。

梳理了一下kylin項目的引用,發現大多數引用都是靜態成員變量,但有幾個類比如DataTypeSerializer,其成員變量也使用了InternalThreadLocal,這就有可能會內存泄漏。理論上將用在對象成員變量上的引用改回ThreadLocal問題就解決了,但穩妥起見,還是應該先驗證一下我們的猜想是否正確。

驗證猜想

使用BTrace來驗證我們的猜想。BTrace腳本可以加攔截代碼,打印方法調用的上下文。把BTrace腳本掛到JVM上,不用改變線上代碼,不用重啓,就可以輸出我們想要的日誌,是定位線上問題的神器。如何使用可自行谷歌,有需要可以給我留言。

通過BTrace腳本的日誌,可以看到構建服務確實會頻繁構建InternalThreadLocal對象,主要是DataTypeSerializer對象初始化時調用的。幾個小時內InternalThreadLocal構建次數就達到了幾十上百萬。而查詢服務就沒有頻繁構建的問題,猜想被驗證正確。

優化結果

將對象成員變量的InternalThreadLocal引用改回ThreadLocal,重啓服務。InternalThreadLocal對象運行幾天也只有十幾次構建,不隨時間增多,沒再出現OOM,問題解決。相關pr已貢獻給kylin社區。

作者:初曉【滴滴出行資深軟件開發工程師】

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