越是忙,雜七雜八的事情越多,最近正在優化Memcache
的客戶端代碼,這時候SIP
突然出現OOM
的問題(Out of Memory
),作開發最頭痛就是這種問題,壓力測試都作過,早期的幾個版本都沒有出現這樣的問題,因此懷疑可能是最近一次發布修改引起的。借助JProfiler
在測試環境搭了一套系統,開始做壓力測試,來分析Memory
到底流到了哪里去了。
問題一:連接池泄漏
看到這個問題,我想很多人都說,都什么年代了,使用開源的現成連接池,怎么還會有這樣的問題,又不是那些使用jdbc的年代了。那來看看現象吧。
場景:測試部用loadRunner往死里壓,發現很多業務對象不斷增長,但是按照業務場景來說,這些業務對象處理以后就自動釋放了。(在本地的開發環境驗證了是會自動釋放的)
JProfiler截圖:

上圖中可以看到有很多業務對象已經累積占用了不少內存,在讓測試部同學停掉壓力測試以后,等待了一會兒,然后用JProfiler主動發起垃圾回收,也看到了Jboss后臺有GC回收的記錄輸出以后,發現這些對象依然存在,也就是說這些對象成為了Memory泄漏的誘因之一。但是就如我所說的,在本地測試以及白盒測試來看,這些對象在一次請求以后,處理完畢一定會被釋放,沒有被其他Map等Reference,然后通過JProfiler看了看這些對象的Allocation Call Tree,就是我們處理請求的Servlet作為源頭的,但為什么Servlet沒有被清理掉呢?接著來看看后面二張圖

既然知道對象存在并且被Hold了,那么就去看看線程運行的狀況,這一看發現有很多線程都處于Wait的狀態(其實在server作dump也可以看到),這張圖上就可以看到,我選擇了其中一個wait的線程它處于等待狀態的原因就是在ibatis的Throttle作increment的時候處于等待狀態,看了看ibatis的代碼,這部分代碼其實是ibatis連接池的一段代碼,在連接池被占滿以后,處于等待釋放的狀態,也就是說程序把連接池耗盡了。
為了驗證是否是耗盡了,讓DBA老大光輝給我看了看MySql(這部分當天的日志數據都保存在MySql中)的連接情況,發現只有8個連接,看來不是真的耗盡,應該是連接池泄露了。光輝告訴我,這八個連接都在做同一個查詢,就是統計某一個API的訪問記錄次數和流量。在當前的業務流程中對于MySql主要做了兩類操作:
1.訪問控制計數器創建的統計查詢。
由于要對Open API訪問控制,采用了Memcache計數器方式來實現。當發現此類API沒有創建過計數器,那么就分析MySql中的數據,創建計數器,后續的訪問記錄除了插入數據庫以外還需要累加計數器,這樣訪問控制可以高效使用集中式計數器而不需要查詢數據庫。
2.日志批量異步寫入。
對于Open API的記錄采用了線程池中每一個線程維護一個內存分頁,當頁滿或者到了刷新間隔時,一次性批量寫入數據庫,緩解數據庫寫入壓力,這里采用了事務來批量提交。
對于第一種操作,由于設計中MySql就只會保留當天的數據量,因此只有系統啟動的時候做一次統計,對于數據庫壓力和Sql執行來說應該沒有太大的壓力,但是由于壓力測試是從昨天下午就開始做的,里面的數據已經有上千萬,因此這次重新啟動開始做壓力測試,導致了這個創建計數器的Sql執行很慢。同時日志的批量寫入采用的是事務方式來提交,對于MySql其實自己還不是很深入,但是感覺上來說,問題應該出現在這里,由于查詢的緩慢在加上事務批量的提交,可能會造成事務失敗,同時沒有正確的將釋放資源的信號傳遞給ibatis,導致了看起來的連接資源耗盡。
我將數據庫中的記錄全部刪除,然后重新啟動,開始壓力測試,問題不存在了,對象都及時得到回收。后續還會去跟進這個問題,在ibatis早期版本,同樣是這個類出現了死鎖的問題,后來升級得到了解決,但是也看到很多國外的朋友說道2.2和2.3其實還是有死鎖的問題,不過我個人覺得可能還是和數據庫也有一定關系。
疑問:
這個問題的背后我還有一點疑問,對于我們來說,如果一個普通的http請求,當超時以后肯定就會自動中斷,但是在這個場景中,我足足等了1個小時還是沒有釋放,也就是說客戶端其實已經斷開了,但是JBoss好像并不會釋放這些處理請求的事務,導致資源被卡。
問題二:LinkedBlockingQueue惹禍
自從Jdk1.5以后concurrent包為大家提供了很多便利高效的開發新模式,我在不少地方用到了LinkedBlockingQueue,作為消費者和生產者之間的數據通道,消費者們等待在LinkedBlockingQueue門口守候生產者提供數據,獲取數據后就開始并行處理。這里我會采用queue.poll(100,TimeUnit.MILLISECONDS)這種方式來半阻塞的獲取數據。其實在昨天已經聽說LinkedBlockingQueue可能存在著內存泄露的問題,看了看很多網上的人也都提到了這個問題,在1.5種沒有得到解決,在1.6中會去fix這個問題,但是沒有證據,也不好亂加斷定。在問題一搞好以后,然后繼續查找潛在bug,這時候不經意的發現有一個對象隨著時間的推移始終在增加,但是由于單個對象占的內存不大,因此沒有很明顯的體現出來,但是對象實例的增加卻是很明顯的,看看下面兩張圖:
這兩張圖的間隔時間2小時左右,可以發現這個對象的instance已經有了很大的增長,同時內存也吃了不少,看了看創建這個對象的Tree,發現就是poll這個方法,也就是我線程池中線程周期性掃描的結果。這期間沒有任何訪問,僅僅就是放著不動,就有如此大量的增長。我嘗試將poll(100,TimeUnit.MILLISECONDS)換成poll()全阻塞方式,對象增長依舊。因此可以看出來服務器的Memory Leak很大程度上由這部分引起,早先沒有發現,因為是SIP上線不久,沒有太多用戶,而這陣子用戶越來越多,加上API中的更新類請求比較吃內存,就容易發現此類問題。
那么是否1.6就解決了這個問題呢,開始使用機器上1.6_01的版本,發現問題依舊,去sun下載了最新的1.6_07,發現的卻會回收,但是回收和增長都存在,具體數據描述舉例如下:
1. 1000 instance 31k
2. 200 instance 6k (回收了一部分)
3. 1500 instance 46k(發現增長的比以前還多)
4. 300 instance 9k (回收了一部分)
5. 2000 instance 62k (發現增長的比以前還多)
也就是說,回收時有發生,但是總體趨勢還是在上升,這個真的還需要好好測試,有興趣的同學也可以試驗一下我的測試方式,就僅僅只需要使用一個LinkedBlockingQueue,然后定時的去pool,1.5絕對增長的不小。
對于這個問題,我只能再去驗證,如果發現真的暫時不可避免,那么只有考慮替代方案了。
這是今天作了Memory Leak的一些分享,希望也能給其他遇到或者將會遇到問題的同學一個分享,說一句,如果有條件的話用JProfiler去分析性能絕對是不錯的,沒有條件么就dump,gc輸出來查找問題。
剛剛作了測試現在的場景可以用take來替換poll,原來是看中了poll的timeout方式,take完全沒有問題,看來如果要在1.5版本用,還是老老實實用take。