天天看點

利用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日

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

圖二

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

解決方法

方法如下:

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

發現如下:

圖三

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖三所示,記憶體經過HttpSession逾時後,并強制gc後,仍然有大量的對象沒有釋放。例如:gov.gdlt.taxcore.comm.security.MenuNode,仍然有807個執行個體沒有釋放。

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

圖四

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

我們發現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中不會被釋放,最終造成了記憶體的洩漏。

圖五

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖六

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖七

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖八

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如上圖所示,最頂層(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,比較符合目前記憶體洩漏的情況。當然,這種估計仍然需要經過實踐的檢驗,方法是:當這次發現的記憶體洩漏問題解決後看系統是否還有其它記憶體洩漏問題。

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

方案

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記憶體)。
利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

實施情況

采用的方案:某某軟體商采用了新的會話登入資訊存貯方案,即:ExternalSessionManager的成員變量sessionMap中不再儲存使用者菜單資訊,隻儲存基本的登入資訊;存儲方式采用使用者id(11位)作為鍵值(key)來保留使用者基本登入資訊。

基本分析:由于基本登入資訊隻有1K左右,而目前内網登入的使用者總數也隻有8887個,是以隻儲存了大約10M-15M的資訊在記憶體,占用量很小,并且不會有記憶體洩漏。使用者菜單資訊儲存在session中,如果使用者退出時點選logout頁面,那麼應用伺服器可以很快地釋放這部分記憶體;如果使用者直接關閉視窗,那麼儲存在session中的菜單資訊隻有等會話逾時後才會由系統清除并回收記憶體。

監控狀況:

圖九

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖九所示,ExternalSessionManager中隻保留了簡單的登入資訊(Map中儲存了WHsessionAttrVO對象),包括:目前版本(currentversion),操作人員代碼基本資訊(czrydmInfo),目前時間(currenttime)。

圖十

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖十所示,這個登入使用者的基本資訊隻有1368 bytes,大約1.3K

圖十一

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖十二

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖十二所示,當另外一個會話逾時後,應用伺服器回收了整個會話的菜單資訊(MenuNode),圖上已經沒有MenuNode對象了。并且由于是同一個使用者登入,是以保留在ExternalSessionManager成員變量sessionMap中的對象WHsessionAttrVO隻有一個(id=24400001129),而沒有産生多個,沒有因為多次登入而産生多個對象的後果,避免了記憶體洩漏問題的出現,解決了前期定位的記憶體洩漏問題。

圖十三

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖十三所示,經過gc記憶體回收後,發現記憶體回收比較穩定,基本都回收到了最低點,也證明了記憶體沒有洩露。

結論與建議:從測試情況看,解決了前期定位的記憶體洩漏問題。

生産系統實施後的監控與分析

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

圖十四 2005年6月2日

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖十五 2005年6月3日周五

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖十六2005年6月5日周日

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖十七 2005年6月6日周一

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

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

圖十八 2005年6月7日周二

利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

如圖十八所示,在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應用伺服器沒有出現當機重新開機的現象。
利用JProfiler對應用伺服器記憶體洩漏問題診斷一例

總結

通過本文,我們可以看到,記憶體的洩露将會導緻伺服器的當機,系統性能就更别說了。對于系統記憶體洩露問題應該從伺服器GC日志方面進行早診斷,使用工具早确認并提出解決方案,排除記憶體洩露問題,提高系統性能,以規避項目風險。

原作者:曾勝财 , IBM BCS 部門 I/T架構師