一、背景
最近新服務上線,運行了一段時間都很平穩,沒有出現什么大的異常,突然有一天運維同事通知說注冊中心上服務掉線了。於是登錄了發生異常服務的組件,查看日志信息,關鍵信息如圖:
從上面兩個圖片可以簡單了解到,
1、應該是服務發生了OOM異常
2、Consul鏈接因為Connection pool shut down而鏈接失敗。
二、問題分析
猜想1:是不是因為Consul服務宕機,導致服務鏈接不上,並且短時間重試多次造成OOM?
實際:整個錯誤持續了幾個小時,consul服務宕機其他服務也應該收到影響,但是其他服務並沒有問題。所以猜想1應該不對。
猜想2:Connection pool shut down 這個問題是因為什么產生的?是不是可以從該地方入手,根據圖中的異常位置可以查看代碼。查看對應的
繼續查看
所以一定是因為 isShutDown 這個狀態值變為了true,才導致打印對應的錯誤信息的。
繼續查找shutdown可能賦值為true的地方
最終定位到能夠調用shutdown只有finalize()或者close()。
所以猜想一定是發生OOM之后,GC在回收內存的時候調用了finalize()方法,繼而引發的isShutDown變為true。
猜想3:我們是否可以通過dump文件,查看內存泄漏的地方,進而在本地模擬這個場景。找到運維下載OOM時候的dump文件,經過MAT工具一番查看,可以很明顯的找到內存泄漏的點。
如圖所示:據dump文件描述,PoolingHttpClientConnectionManager 這個對象居然占到了整個大對象內存的90%多,並且是被一個數組對象引用着的,引用的對象數量居然有60萬。繼續查看詳細:可以看到這個對象應該是在 com.aliyun.oss.common.comm.IdleConnectionReaper 這個類當中,我們可以看下這個類的源碼,可以確認應該就是下面這個靜態對象列表。
查看該列表使用的地方,會發現在registerConnectionManager方法內進行填充
registerConnectionManager方法調用的地方,
最終定位,果然是在創建OSSClient的時候調用的
OssClient構造函數
我們看下OssClient是如何創建的,為什么connectionManagers列表會持有這么多對象。最終發現在上傳圖片的過程中,我們每調用一次getOSSClient()就會生成一個OssClient對象,就會在connectionManagers添加一個PoolingHttpClientConnectionManager對象。
最終問題定位應該就是這里,因為沒有及時關閉OssClient導致的內存泄漏,因為不是一瞬間就造成內存暴漲,而是緩慢的增加,所以沒有第一時間發現,這也是為什么重啟之后就沒有報錯的原因。
三、問題重現
我們本地啟動服務,修改一下jvm參數如下(比線上減少了10倍)
-Xms200M -Xmx200M -Xss256K
順便增加一些測試代碼,打印一下當前jvm的內存信息
啟動程序,模擬線上上傳圖片:
隨着請求數量增加,可以發現connectionManagers中PoolingHttpClientConnectionManager對象也在不斷上漲。
加大請求次數,通過VirtualGC查看內存分布,也可以發現 老年代一直在上漲,幾次GC之后也沒有明顯下降
最終在我們還可以看到
1、出現了OOM異常
2、Consul的 Connection pool shut down 也被觸發了
綜上所有的跡象都說明就是因為OSSClient這個類被一直new,導致內存不斷泄漏、最終導致程序OOM使得出現了很多異常。
四、問題解決
因為本服務需要提供用戶上傳圖片到阿里雲OSS的功能,並且調用頻次還算比較高,所以解決方式有兩種。
1、每次new OSSClient之后,需要調用shutdown方法,移除PoolingHttpClientConnectionManager對象。
我們都知道new操作是一個比較耗費資源和性能的操作,而且什么時候shutdown不好控制,所以我覺得這樣的方式比較適用於操作比較少的場合。
2、使用單例的OSSClient,看了剛剛的代碼OssClient本身也封裝了一個PoolingHttpClientConnectionManager對象,說明它本身就是支持鏈接池的,也就是可以並發訪問。
修正為單例之后,執行測試:
可以看到經過一段時間的請求,整體老年代內存維持不高。
四、問題總結
經過這個bug的排查,可以有以下幾點收獲:
1、遇到OOM問題,僅憑借日志輸出可能不能馬上定位到問題的本質,最好可以通過dump文件進行分析
2、像HttpClient這種需要建立socket鏈接的資源,在使用的時候盡量能夠池化,避免大量創建對象耗費資源,並且使用后要考慮關閉的問題。