常用鏈接

          統計

          最新評論

          利用JProfiler對應用服務器內存泄漏問題診斷一例(轉)

                 在中間件應用服務器的整體調優中,有關于等待隊列、執行線程,EJB池以及數據庫連接池和Statement Cache方面的調優,這些都屬于系統參數方面的調優,本文主要從另外一個角度,也就是從應用的角度來解決中間件應用服務器的內存泄露問題,從這個角度來提高系統的穩定性和性能。

          項目背景

          問題描述

          某個大型項目(Use Case用例超過300個),在項目上線后,其Web應用服務器經常宕機。表現為:

          1. 應用服務器內存長期不合理占用,內存經常處于高位占用,很難回收到低位;

          2. 應用服務器極為不穩定,幾乎每兩天重新啟動一次,有時甚至每天重新啟動一次;

          3. 應用服務器經常做Full GC(Garbage Collection),而且時間很長,大約需要30-40秒,應用服務器在做Full GC的時候是不響應客戶的交易請求的,非常影響系統性能。

          Web應用服務器的物理部署

                一臺Unix服務器(4CPU,8G Memory)來部署本Web應用程序;Web應用程序部署在中間件應用服務器上;部署了一個節點(Node),只配置一個應用服務器實例(Instance),沒有做Cluster部署。

          Web應用服務器啟動腳本中的內存參數
          MEM_ARGS="-XX:MaxPermSize=128m -XX:MaxNewSize=512m -Xms3096m
          -Xmx3096m -XX:+Printetails -Xloggc:./inwebapp1/gc.$$"

          可以看出目前生產系統中Web應用服務器的內存分配為3G Memory。

          Web應用服務器的重要部署參數
          參數名稱 參數值 參數解釋
          kernel.default(Thread Count) 120 執行線程數目,是并發處理能力的重要參數
          Session Timeout 240分鐘(4小時) HttpSession會話超時

           

          分析

          分析方法

          內存長期占用并導致系統不穩定一般有兩種可能:

          1. 對象被大量創建而且被緩存,在舊的對象釋放前又有大量新的對象被創建使得內存長期高位占用。

          • 表現為:內存不斷被消耗、在高位時也很難回歸到低位,有大量的對象在不斷的創建,經過很長時間后又被回收。例如:在HttpSession中保存了大量的分頁查詢數據,而HttpSession的會話超時時間設置過長(例如:1天),那么在舊的對象釋放前又有大量新的對象在第二天產生。
          • 解決辦法:對共享的對象可以采用池機制進行緩存,避免各自創建;緩存的臨時對象應該及時釋放;另一種辦法是擴大系統的內存容量。

          2. 另一種情況就是內存泄漏問題

          • 表現為:內存回收低位點不斷升高(以每次內存回收的最低點連成一條直線,那么它是一條上升線);內存回收的頻率也越來越高,內存占用也越來越高,最終出現"Out of Memory Exception"的系統異常。
          • 解決辦法:定位那些有內存泄漏的類或對象并修改完善這些類以避免內存泄漏。方法是:經過一段時間的測試、監控,如果某個類的對象數目屢創新高,即使在JVM Full GC后仍然數目降不下來,這些對象基本上是屬于內存泄漏的對象了。

          問題定位

          這里請看5月份 Web應用服務器的內存回收圖形:

          《注意:5月18日早上10點重新啟動了Web服務器,5月20日早上又重新啟動了Web服務器。》

          • 在Web應用重要部署參數中,我們知道:Session的超時時間為4個小時,我們在監控平臺也觀測到:在18日晚上10點左右所有的會話都過期了,從圖形一中也能看出18日晚上確實系統的內存有回收到40%(就象股票的高位跳水);
          • 從圖形一(5月18日)中我們也能看到Full GC回收后的內存占用率走勢(紅色曲線),上午基本平滑上升到20%(內存占用率),中午開始上升到30%,下午上升到40%
          • 從圖形二(5月19日)中我們也能看到Full GC回收后的內存占用率走勢(紅色曲線),上午又上升到了60%,到下午上升到了70%。
          • 從黃色曲線(GC花費的時間,以秒為單位),Full GC的頻率也在增快,時間耗費也越來越長,在圖形一中基本高位在20秒左右,到19日基本都是30-40秒之間了。

           圖形一 5月18日
          圖形一 5月18日

          圖二
          圖二

          通過上述分析,我們基本定位到了Web應用服務器的內存在高位長期占用的原因了:是內存泄露!并且正是由于這個原因導致系統不穩定、響應客戶請求越來越慢的。

          解決方法

          方法如下:

          • 我們從圖形二中發現,在8.95(將近9點鐘)到9.66(將近9點40)期間有幾次Full GC,但是有內存泄漏,從占用率40%上升到50%左右,泄漏了大約10%的內存,約300M;
          • 我們在自己搭建的Web應用服務器平臺(應用軟件版本和生產版本一致)做這一階段相同的查詢交易;表明對同一個黑盒(Web應用)施加同樣的刺激(相同的操作過程和查詢交易)以期重現現象;
          • 我們使用Jprofiler工具對Web應用服務器的內存進行實時監控;
          • 做完這些交易后,用戶退出系統,并等待Web應用服務器的HttpSession超時(我們這里設置為15分鐘);
          • 我們對Web應用服務器做了兩次強制性的內存回收操作。

          發現如下:


          圖三
          圖三

          如圖三所示,內存經過HttpSession超時后,并強制gc后,仍然有大量的對象沒有釋放。例如:gov.gdlt.taxcore.comm.security.MenuNode,仍然有807個實例沒有釋放。

          我們繼續追溯發現,這些MenuNode首先存放在一個ArrayList對象中,然后發現這個ArrayList對象又是存放在WHsessionAttrVO對象的Map中,WHsessionAttrVO 對象又是存放在ExternalSessionManager的staic Map中(名稱為sessionMap),如圖四所示。


          圖四
          圖四

          我們發現gov.gdlt.taxcore.taxevent.xtgl.comm.WHsessionAttrVO中保存了EJBSessionId信息(登錄用戶的唯一標志,由用戶id+登錄時間戳組成,每天都不同)和一個HashMap,這個HashMap中的內容有:

          • ArrayList: 內有MenuTreeNodes(菜單樹節點)
          • HashMap: 內有操作人員代碼信息
          • CurrentVersion:當前版本號
          • CurrentTime:當前系統時間

          WHsessionAttrVO這個對象的最終存放在ExternalSessionManager的static Map sessionMap中,由于ExternalSessionManager是一個全局的單實例,不會釋放,所以它的成員變量sessionMap中的數據也不會釋放,而Map中的Key值為EJBSessionId,每天登錄的用戶EJBSessionId都不同,就造成了每天的登錄信息(包括菜單信息)都保存在sessionMap中不會被釋放,最終造成了內存的泄漏。


          圖五
          圖五

          如上圖所示:WHsessionAttrsVO對象中除了有一個String對象(內容是EJBSessionId),還有一個HashMap對象。


          圖六
          圖六

          如上圖所示,這個HashMap中的內容主要有menuTreeNodes為key,value為ArrayList的對象和以czrydminfo為key,value為HashMap對象的數據。


          圖七
          圖七

          如上圖所示:menuTreeNodes為key,value為ArrayList對象中包含的對象有許多的MenuNode對象,封裝的都是用戶的菜單節點。


          圖八
          圖八

          如上圖所示,最頂層(Root)的初始對象為一個ExternalSessionManager對象,其中的一個成員變量為static (靜態的),名稱為:sessionMap,這個對象是singleton方式的,全局只有一個。

          初步估量

          我們從圖形一和圖形二中可以看出,每天應用服務器損失大約40%的內存,大約1G左右。

          從圖形四可以看出,當前用戶(Id=24400001129)有807個菜單項(每個菜單項為一個MenuNode 對象實例,圖形四中的這個實例的size為592 Byte),這些菜單數據和用戶基本登錄信息(czrydmInfo HashMap)也都存放在WHsessionAttrVO對象中,當前這個WHsessionAttrVO對象的size為457K。

          我們做如下估算:

          假設平均每天有4千人(估計值,這個數值僅僅是5月19日峰值的1/2左右)登錄系統(有重復登錄的現象,例如:上午登錄一次,中午退出系統,下午登錄一次),以平均每人占用200K(估計值,是用戶id=24400001129 的Size的1/2左右)來計算,一天泄漏的內存約800M,比較符合目前內存泄漏的情況。當然,這種估計仍然需要經過實踐的檢驗,方法是:當這次發現的內存泄漏問題解決后看系統是否還有其它內存泄漏問題。


          方案

          ExternalSessionManager類是當初某某軟件商設計的用來解決Web服務器負載均衡的模塊,這個類主要用來保存客戶的基本登錄信息(包括會話的EJBSessionId),以維護多個Web服務器之間的會話信息一致。

          改進方案有兩種:

          • 從架構設計方面改進

            實現Web層的負載均衡有很多標準的實現方式。例如:采用負載均衡設備(硬件或軟件)來實現。

            如果采用新的Web層的負載均衡方式,那么就可以去掉ExternalSessionManager這個類了。

          • 從應用實現方面改進

            保留當前的Web層的負載均衡設計機制,僅僅從應用實現方面解決內存泄漏問題,首先菜單信息不應該保存在ExternalSessionManager中。其次,增加對ExternalSessionManager類中用戶會話登錄信息的清除,有幾種方式可以選擇:

            • 被動方式,當HttpSession會話超時(或過期)被Web應用服務器回收時清除相應的ExternalSessionManager中的過期會話登錄信息。
            • 主動方式,可以采用任務定時清理每天的過期會話登錄信息或線程輪詢清理。
            • 采用新的會話登錄信息存儲方式,ExternalSessionManager的sessionMap中的key值不再以EJBSessionId作為鍵值,而是以用戶id(EJBSessionId的前11位)代替。由于用戶id每天都是一樣的,所以不會造成內存泄漏。保存得登錄信息也不再包含菜單節點信息,而只是登錄基本信息。最多也只是保存整個系統所有的用戶id及其基本登錄信息(大約每個用戶的登錄信息只有1.5K左右,而目前這個系統的營業網點用戶為1萬左右,所以大約只占用Web服務器15M內存)。


          實施情況

          采用的方案:某某軟件商采用了新的會話登錄信息存貯方案,即:ExternalSessionManager的成員變量sessionMap中不再保存用戶菜單信息,只保存基本的登錄信息;存儲方式采用用戶id(11位)作為鍵值(key)來保留用戶基本登錄信息。

          基本分析:由于基本登錄信息只有1K左右,而目前內網登錄的用戶總數也只有8887個,所以只保存了大約10M-15M的信息在內存,占用量很小,并且不會有內存泄漏。用戶菜單信息保存在session中,如果用戶退出時點擊logout頁面,那么應用服務器可以很快地釋放這部分內存;如果用戶直接關閉窗口,那么保存在session中的菜單信息只有等會話超時后才會由系統清除并回收內存。

          監控狀況:


          圖九
          圖九

          如圖九所示,ExternalSessionManager中只保留了簡單的登錄信息(Map中保存了WHsessionAttrVO對象),包括:當前版本(currentversion),操作人員代碼基本信息(czrydmInfo),當前時間(currenttime)。


          圖十
          圖十

          如圖十所示,這個登錄用戶的基本信息只有1368 bytes,大約1.3K


          圖十一
          圖十一

          如圖十一所示,一共有兩個用戶(相同的用戶id)登錄系統,當一個用戶使用logout頁面退出時,保留在session中的菜單信息(MenuNode)立刻釋放了,所以Difference一欄減少了806個菜單項。


          圖十二
          圖十二

          如圖十二所示,當另外一個會話超時后,應用服務器回收了整個會話的菜單信息(MenuNode),圖上已經沒有MenuNode對象了。并且由于是同一個用戶登錄,所以保留在ExternalSessionManager成員變量sessionMap中的對象WHsessionAttrVO只有一個(id=24400001129),而沒有產生多個,沒有因為多次登錄而產生多個對象的后果,避免了內存泄漏問題的出現,解決了前期定位的內存泄漏問題。


          圖十三
          圖十三

          如圖十三所示,經過gc內存回收后,發現內存回收比較穩定,基本都回收到了最低點,也證明了內存沒有泄露。

          結論與建議:從測試情況看,解決了前期定位的內存泄漏問題。

          生產系統實施后的監控與分析

          經過調優后,我們發現:在2005年6月2日晚9點40左右重新部署、啟動了Web應用服務器(采用了新的調優方案)。經過幾天的監控運行,發現Web應用服務器目前運行基本穩定,目前沒有出現新的內存泄漏問題,下列圖示說明了這一點


          圖十四 2005年6月2日
          圖十四 2005年6月2日

          如圖十四所示,6月2日晚21.7(21點42分)重新啟動應用服務器,內存占用很少,大約為15%(請看紅色曲線),每次GC消耗的時間也很短,大約在5秒以內(請看黃色曲線)。


          圖十五 2005年6月3日周五
          圖十五 2005年6月3日周五

          如圖十五所示,在6月3日周五的整個工作日內,內存的回收基本到位,回收位置控制在20%-30%之間,也就是在600M-900M之間(請看紅色曲線的最低點),始終可以回收2G的內存供應用程序使用,每次GC的時間最高不超過20秒,Full GC平均在10秒左右,時間消耗比較短(請看黃色曲線)。


          圖十六2005年6月5日周日
          圖十六2005年6月5日周日

          如圖十六所示,在周日休息日期間,Web應用服務器全天只做了大約4次Full GC(黃色曲線中的小山峰),時間都在10秒以內;大的Full GC后,內存只占用10%,內存回收很徹底。


          圖十七 2005年6月6日周一
          圖十七 2005年6月6日周一

          如圖十七所示,在周一工作日期間,內存回收還是不錯的,基本可以回收到30%(見紅色曲線的最低點),即:占用900M內存空間,剩余2G的內存空間;Full GC的時間大部分控制在20秒以內,平均15秒(見黃色曲線)。


          圖十八 2005年6月7日周二
          圖十八 2005年6月7日周二

          如圖十八所示,在6月7日周二早上,大約8:30左右,Web應用服務器作了一次Full GC,用了10秒的時間,把內存回收到了10%的位置,為后續的使用騰出了90%的內存空間。內存回收仍然比較徹底,說明基本沒有內存泄漏問題。

          經過這幾天的監控分析,我們可以看出:

          • Web應用服務器的內存使用已經比較合理,內存在工作日的占用在20%至30%之間,約1G的內存占用,有2G的內存空間富裕;而在空閑時間(周日,每天的凌晨等)內存可以回收到10%,有90%的內存空間富裕;
          • Web應用服務器的Full GC的次數明顯減少了并且每次Full GC占用的時間也很少,基本控制在10-20秒之間,有的甚至在10秒以內,明顯改善了內網應用服務器內存的使用
          • 從6月2日重新部署之后,Web應用服務器沒有出現宕機重啟的現象。

           

          總結

           通過本文,我們可以看到,內存的泄露將會導致服務器的宕機,系統性能就更別說了。對于系統內存泄露問題應該從服務器GC日志方面進行早診斷,使用工具早確認并提出解決方案,排除內存泄露問題,提高系統性能,以規避項目風險。

          posted on 2008-04-22 13:24 九寶 閱讀(330) 評論(0)  編輯  收藏 所屬分類: Java

          主站蜘蛛池模板: 石泉县| 郁南县| 广平县| 榕江县| 梨树县| 辉县市| 海兴县| 新泰市| 应用必备| 静安区| 衡阳县| 上蔡县| 洞口县| 漳浦县| 清丰县| 泰州市| 锡林郭勒盟| 托克托县| 安图县| 丁青县| 昌邑市| 馆陶县| 怀远县| 延安市| 陈巴尔虎旗| 西乡县| 大名县| 延吉市| 松滋市| 商南县| 鄯善县| 于田县| 高邑县| 五河县| 金门县| 桃江县| 惠安县| 富阳市| 临汾市| 涞源县| 三都|