近期需要對公司的接口做線上的巡查監控,需要寫一個腳本放到服務器上,定時運行腳本監測線上接口是否正常。
測試的接口不是HTTP協議,而是公司基於TCP協議開發的私有協議,因此不能直接用現成的一些接口測試工具,需要自己寫代碼來調用接口。
由於是私有協議,為了方便各業務項目進行通信,開發部門統一提供了一個TClient的jar包,底層使用了netty框架進行通信。調用方只需要按照協議的格式組裝二進制的包,然后直接調用TClient的sendMessage方法就可以把數據發送出去,服務端處理完成后會異步回調,將響應數據返回給客戶端。
腳本寫完了,偽代碼如下
public class Demo{
public void invoke(){
// 創建TClient並初始化
TClient client = new TClient(xxx);
// 組裝接口數據包
Data data = new Data(xxx);
// 發送數據
Response res = client.sendMessage(data);
// 檢查結果、存儲結果、發送郵件
doSomething();
// 關閉client
client.close();
}
}
測試腳本中,每隔一分鍾,創建一個Demo對象,調用invoke方法
Demo demo = new Demo();
demo.invoke();
腳本寫好后在服務器上調試了下,接口返回數據正常,於是正式啟動定時任務,觀察了一會,運行一切正常,Perfect!
第二天早上到公司,登上服務器,查看昨晚腳本的運行情況,看了下日志。
打開日志我就震驚了,What?OutOfMemoryError!竟然內存泄漏了!
平常都是開發寫bug時出現內存泄漏,今天終於輪到我自己了!
最后一條日志顯示為下午17:17左右,也就是腳本大概運行了4小時后出現了內存泄漏。查看了下腳本進程,果然已經崩潰了,並且生成了一個dump文件。
經常做性能測試的同學,對內存泄漏都不陌生。內存泄漏總結來說就是JVM中存儲的對象太多了,占滿了全部內存空間,並且這些對象都是不可回收的。這樣程序就不能再繼續運行了,因為已經沒有空間了。
舉個例子,就好像去飯館吃飯,飯館里總是不斷的有人進去,也有人出來。如果某天來了一幫人占滿了飯店,並且賴着不走了,這樣新顧客就進不來了,這個時候估計老板就崩潰了。
我先review了腳本的代碼,沒發現什么異常的問題。有的朋友可能會說,你不是每個1分鍾創建一個Demo對象嗎,運行這么長時間,會不會是Demo對象太多了?
其實並不會,寫腳本的時候也考慮過這個問題,每次new Demo對象,因為上一次腳本已經執行完了,那么上一次的Demo對象就沒有引用了,這樣JVM在垃圾回收的時候會把上一次的Demo對象清理掉。這樣並不會造成內存泄漏。
目光再回到服務器上,Java進程在崩潰時,自動生成了一個堆dump文件,如果已經發生了內存泄漏,可以分析這個dump文件,看看里面那些對象比較多,這樣就能確定原因了。
一般在工作中分析內存泄漏時,可以把dump文件下載到本地,然后通過jvisualvm或者jprofiler打開文件,工具自動會分析哪些對象數量最多。
但是這個文件有1.3G,公司服務器下載有限速,想下載下來估計得等到7月7號testfan性能測試實戰班開課那天了。
突然想到另外一個分析內存泄漏的工具MAT,之前都是在windows下使用MAT,其實MAT也有Linux版本,可以直接在服務器上對dump文件進行分析。
簡單介紹下工具的使用方法:
1、 登錄官網,下載Linux x86_64/GTK+版本
https://www.eclipse.org/mat/downloads.php
2、 解壓后修改MemoryAnalyzer.ini配置文件,配置jvm參數(要比dump文件大)
3、 執行.mat提供的腳本
./ParseHeapDump.sh /home/xxx.hprof org.eclipse.mat.api:suspects
(/home/xxx.hprof是dump文件的路徑)
4、 在xxx.hprof目錄下,生成了java_pid32523_Leak_Suspects.zip壓縮文件
5、 下載到windows下,解壓,打開index.html
在分析頁面中可以看到,io.netty.channel.nio.NioEventLoopGroup對象占用了JVM中61.36%的空間。其次是io.netty.buffer.PoolThreadCache對象,占用了21.38%。
看名字這倆對象都是netty框架中的類,在網上查了下資料,“NioEventLoopGroup”是netty中的一個線程池對象。
看頁面上的統計,JVM中有1145個netty的線程池對象,這是什么操作?線程池不就一個就行了嗎?為什么有這么多?
看到線程池對象,就想到會不會JVM線程方面有問題?因為腳本進程現在已經崩潰了,只能重新運行腳本,然后再對線程進行監控。
腳本運行過程中,通過監控jvm,發現old區確實在不斷的緩慢增加,這樣長時間跑下去,應該就會重現昨天晚上的問題。
執行jstack命令打印線程堆棧信息
jstack pid > thread.log
打開thread.log看了下,線程狀態倒沒啥問題,但是堆棧中有大量的nioEventLoopGroup線程,看編號有1000+,通過命令統計了下,確實有1000+個nioEventLoopGroup線程。
這個數量跟上面MAT工具分析的實例數量也差不多對應上了,現在問題基本上就確定了。也就是說在內存泄漏發生前,JVM中存在1000+個nioEventLoopGroup線程,每個線程創建了一個NioEventLoopGroup對象,因為線程池的特性,所以這些線程處於都是運行狀態的。
並且在腳本運行過程中發現,這個nioEventLoopGroup線程並不是開始就是1000+,而是從0慢慢漲上來的。也就是說隨着腳本的運行,慢慢積累上來的。
這個時候目光又回到了我的腳本中,雖然並不是因為我不斷的new Demo對象造成了內存泄漏,但是肯定跟這個行為有關系,nioEventLoopGroup是netty框架用到的對象,於是就想到了代碼中的TClient client = new TClient(xxx);
打開TClient的jar包看了下,在TClient的構造函數里,確實創建了一個nioEventLoopGroup對象
然后在connect方法中,使用了這個線程池對象bossGroup
現在基本上確定是什么原因了,如下:
a> 每隔1分鍾,腳本會new一個Demo對象
b> Demo對象的invoke方法里又new了一個TClient對象
c> TClient對象內部在做netty連接初始化的時候,創建了NioEventLoopGroup線程池對象
雖然腳本中創建的Demo對象和TClient對象都會被JVM回收,但是可能是因為netty使用NioEventLoopGroup線程池和服務端建立了長連接,導致線程池對象並不會被回收。這樣長時間跑下來,JVM中中的NioEventLoopGroup對象就會越來越多,最終導致了內存泄漏。
這么來看,還真是每次new Demo間接帶來的影響。知道原因就好說了,Demo對象不能在每次運行的時候創建,而且放在類初始化的時候創建一個。無論腳本跑多少次,都只有一個NioEventLoopGroup對象了。
重新修改了下腳本,長時間運行監控了下,確實內存使用很穩定,沒有出現內存泄漏的情況。
問題似乎是得到了解決,但是等等。我腦海中突然又想到另外一個問題,雖然我在腳本中每次都創建一個TClient對象,但是每次跑完后,都會調用TClient的close方法啊,close方法里應該會釋放NioEventLoopGroup對象啊,難道沒做嗎?
打開TClient的jar包看了下close方法
在close方法中,確實把NioEventLoopGroup置為null了,對於一個普通的對象來說,只要對象引用為null,那么在下次JVM垃圾回收的時候,就會把這個對象回收掉。但是對於一個線程池對象來說,因為線程池中有活動線程存在,所以盡管置為null了,JVM也不會回收這個線程池。一般的線程池對象,都是通過shutdown方法來銷毀線程池的。
查看了下netty的api文檔,確實有shutdownGracefully方法(優雅關閉)
現在問題徹底搞清楚了,TClient的close方法中,只是簡單的將線程池對象置為null,並沒有進行shutdown操作,因此JVM並不能回收線程池對象。從而造成了,即便用戶調用了close方法,其實資源也沒有銷毀,最終自然就會出現內存泄漏。
作為一個通用的工具包,內部的資源的釋放,並不能靠調用者來保證。理論上來說,即便我每次都new TClient對象,只要我都關閉了。在業務層面來說,也是正常行為。不能讓調用者必須緩存client對象,否則就會出現內存泄漏,這樣是不合理的。
在跟相關開發溝通后,對代碼做了修改,加上了shutdown方法,仍然用老的腳本進行測試,在長時間的運行后,內存依然保持正常。因此這個問題終於解決了。
最后總結一下
1、 此問題的根本原因是client包中close方法沒有成功銷毀資源
2、 理論上來說,重復創建大量對象並不會造成內存泄漏,但是如果代碼中同時也創建了第三方包的對象,在不了解其實現細節的情況了,可能其內部會創建一些不可被回收的對象,這個時候就會有內存泄漏的風險。因此還是盡量的復用對象,減少內存泄漏問題的發生。
作 者:Testfan 北河
出 處:微信公眾號:自動化軟件測試平台
版權說明:歡迎轉載,但必須注明出處,並在文章頁面明顯位置給出文章鏈接