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

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

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