天天看點

Windbg分析高記憶體占用問題

最近産品釋出大版本更新檔更新,一商超客戶更新後,回報系統經常奔潰,導緻超市的收銀系統無法正常收銀,現場排隊付款的顧客更是抱怨聲聲。為了緩解現場的情況, 客戶都是手動回收IIS應用程式池才能解決。

這樣的後果是很嚴重的,接到回報,第一時間想到的是加記憶體吧,這樣最快。但是客戶從8G-->16G-->32G,隻是延長了每次奔潰的時間,但是并沒有解決系統卡頓的問題。到這裡,也基本猜測了問題所在了,肯定是什麼東西一直在吃記憶體且得不到釋放。這種問題,也就隻能打Dump分析了。

遠端客戶應用伺服器,32G記憶體占用已經消耗了78%,而現場已經回報收銀系統接近奔潰了,要求先強制回收記憶體。反正也要奔潰了,先打Dump再說吧。

(PS:打Dump會挂起程序,導緻應用無法響應!而打Dump的耗時,也是根據當時程序的記憶體占用有關,記憶體占用越大,耗時越久。)

打開任務管理器,選擇對應的IIS程序,右鍵建立轉儲檔案(Dump)。

結果,Dump檔案是生成的,結果當分析的時候,發現Windbg提示Dump無效。說明Dump檔案建立的有問題。觀察任務管理器,發現記憶體占用一下就降下來了,原來是之前的程序直接奔潰了,重新開機了一個W3WP程序。

既然直接從任務管理器無法建立,就使用第三方工具收集Dump吧。經過Goggle,找到一款很好用的Dump收集工具ProcDump,是一個指令行應用,其主要用途是監視應用程式的CPU或記憶體峰值并在峰值期間生成Dump。

因為是高記憶體占用問題,我們使用以下指令來抓取dump:

(PS:可以使用程序名稱,也可以使用程序ID來指定要建立Dump的程序。當有多個相同名稱的程序時,必須使用程序ID來指定!)

procdump w3wp -m 20480 -o D:\Dumps (當記憶體超過20G時抓取一個w3wp程序的MiniDump)

上面就是我踩得第一個坑,因為預設抓取的是MiniDump,很快就抓下來,檔案也很小,正在我得意的時候,Windbg加載Dump分析的時候,發現包含的資訊很少,根本無法進行進一步的分析。

調整建立Dump的指令,添加<code>-ma</code>參數即可建立完整Dump。

procdump w3wp -ma -m 20480 -o D:\Dumps (當記憶體超過20G時抓取一個w3wp程序的完整Dump)

結果再一次,當記憶體占用到達20G,占比80%的時候,Dump再次建立失敗,提示:<code>Procdump Error writing dump file</code>。再一次感覺到絕望。不過至少有錯誤提示,Google一把,果然存在天涯淪落人。Procdump Error writing dump file: 0x80070005 Error 0x80070005 (-2147024891): Access is denied。大緻的意思是說,當90S内Dump檔案沒有成功建立的話(也就意外這w3wp程序被挂起了90s),IIS檢測到w3wp程序挂起超過90s沒有響應就會終止程序,重制建立一個新的程序。好嘛,真是處處是坑。

這個坑,也讓我開始真正停下來思考問題。羅馬不是一日建成的,記憶體也不是一下撐爆的。我幹嘛死腦筋非要到記憶體占用超過80%才去打Dump呢呢呢???!

煥然大悟,如醍醐灌頂。

procdump w3wp -ma -m 8000 -o D:\Dumps (當記憶體超過8000M時抓取一個w3wp程序的完整Dump,并輸出到D:\Dumps檔案夾)

此時記憶體占用在40%左右,這次Dump終于成功建立了。

分析Dump,上WinDbg。如果對WinDbg不了解,可以看我這篇WinDbg學習筆記。

接下來就是一通指令亂敲,我盡量解釋清晰。

使用<code>dumpheap -stat</code>指令檢視目前所有托管類型的統計資訊。從輸出的結果來看:

其中占用記憶體最多當屬<code>System.String</code>類型,接近4G的大小(是不是很吃驚?!)。

其次<code>System.Object[]</code>類型占有1.3G大小。

<code>Kingdee.BOS.JSON.JSONArray</code>類型也大概占用了560M。

我們首先來分析占用最多的<code>System.String</code>類型,看看有什麼發現。

從上面的輸出可以發現:

單個<code>System.String</code>類型最大占用2M以上。

超過200byte的位元組的大小的<code>System.String</code>總大小也不過76M。(是以我們也不必深究大的String對象。)

那我們索性挑一個小點的對象來看看存儲的是什麼字元串,來滿足一下我們的好奇心。

似乎是基礎資料字段資訊。那接下來使用<code>!gcroot</code>指令檢視其對應的GC根,看看到底是什麼對象持有其引用,導緻占用記憶體得不到釋放。

從以上輸出可以看出:

該String類型被一個Hashset所持有。

從<code>Cache</code>關鍵字可以看出該String類型是被緩存所持有。

分析到這裡,我們大緻可以得出一個結論:

String類型占用4G記憶體,絕大多數是由緩存所占用,才導緻String類型得不到釋放。

那我們是不是可以猜測記憶體占用持續走高是不是被緩存撐爆的呢?。

帶着這個疑問我們來繼續分析下<code>Kingdee.BOS.JSON.JSONArray</code>類型。

從輸出結果來看:

滿屏都是40byte的JSONArray。隻能使用<code>Ctrl+Break</code>指令中止輸出。

但為了保險期間,我們來驗證下有沒有100byte以上的<code>JSONArray</code>。

這時我們可以大膽猜測所有的<code>JSONArray</code>對象都是40byte。進而可以得出另一個猜測占用560M記憶體的JSONArray,都具有相似的對象結構。接下來我們來驗證這個猜測。随機選擇幾個對象,看看其内容具體是什麼。

從輸出可以看出:

JSONArray實質是<code>System.Object[]</code>類型。

對應的<code>MethodTable: 00007ffdb9386fc0</code>。

如果你記性好的話,我們應當還記得占用記憶體第二多的就是這個<code>System.Object[]</code>類型,占用1.3G。翻到上面,你可以發現其MethodTable和上面的統計資訊是一緻的。

(PS:到這裡我們是不是可以猜測:<code>System.Object[]</code>占用的記憶體無法釋放,就是由于被<code>JSONArray</code>持有引用導緻的呢?)

既然是數組,就使用<code>!DumpArray</code> 指令來解開數組的面紗。

從以上輸出可以看出,其共有8個子項,我們再随機挑幾個子項看看是什麼内容。

我們可以看到一個字元串内容是<code>FHTZDLB</code>,另一個是<code>發貨通知單清單</code>。看到這,我立馬就條件反射的想到,這不就是我們的菜單資訊嘛。為了驗證我的想法,連續檢視幾個<code>JSONArray</code>,都是相似的内容。

這時,我們繼續發揚敢猜敢做的精神。是不是記憶體被菜單緩存撐爆的?!

為了驗證這一猜測,我們繼續從Dump中尋找佐證。使用<code>~* e!clrstack</code>來看看所有線程的調用堆棧吧。

通過仔細比對發現這麼一條<code>Kingdee.BOS.App.Core.MainConsole.MainConsoleServer.GetMenuArrayForCache(Kingdee.BOS.Context)</code>調用堆棧。從方法命名來看,像是用來擷取菜單數組并緩存。結合前後堆棧的聯系,我們可以大緻得出這樣一個線索:使用者使用WebApi登入後會緩存一份獨立的菜單供使用者使用。

有了代碼堆棧,接下來知道怎麼幹了吧?當然是核實源代碼确定問題啊。

<code>Kingdee.BOS.App.Core.MainConsole.MainConsoleServer.GetMenuArrayForCache(Kingdee.BOS.Context)</code>方法源代碼如下:

我們發現它是用的<code>UserToken</code>來緩存使用者菜單。看到<code>Token</code>,你可能就會條件反射的想到其生命周期。是的,聰明賢惠如你,Token是有生命周期的。也就意味着Token過期後,下次登入還會再次緩存一份菜單。你可能會問Token過期後沒有去清對應的菜單緩存嗎?是的,并沒有。

嚴謹的你,可能又會問Token多久過期?20mins。你眼珠子一轉,接着問,滿打滿算,一個使用者1個小時也就申請3次Token,24小時,也就申請72個Token,一個菜單緩存也就頂多1K,是以一個使用者一天也就最多占用72K。你的網站得有多少并發,才能被這麼多菜單緩存撐爆啊?!

Good Question!!!

是的,客戶的應用場景的并發也就頂多幾百而已。那到底是什麼導緻如此多的菜單緩存呢?

原因是,客戶的第三方用戶端使用WebApi與我們的系統對接。而每次調用WebApi時都會先去調用登入接口,但卻未儲存會話資訊。也就是說,客戶第三方用戶端每次的WebApi調用都會産生一個新的Token。那如果有成千上萬的WebApi請求,也就意味着成千上萬的菜單緩存啊。

好了,點到為止。至此,已經基本定位到問題的根源了。

也許很多同學沒有接觸過WinDbg,覺得其是一個複雜的工具。其實通過本文的案例講解,其無非是通過一系列常見的指令來進行問題跟蹤來定位問題。

最後來簡單總結下,Windbg分析問題的步驟:

建立完整Dump檔案

Windbg加載Dump檔案

根據不同問題類型,使用相關的指令進行分析

耐心分析,抽絲剝繭

邊分析邊猜測邊驗證

結合源碼驗證猜想

修複驗證

推薦連結:你必須知道的.NET Core開發指南

推薦連結:你必須知道的ML.NET開發指南

推薦連結:你必須知道的Office開發指南

推薦連結:你必須知道的IOT開發指南

推薦連結:你必須知道的Azure基礎知識

推薦連結:你必須知道的PowerBI基礎知識

Windbg分析高記憶體占用問題
<b></b> 關注我的公衆号『微服務知多少』,我們微信不見不散。 閱罷此文,如果您覺得本文不錯并有所收獲,請【打賞】或【推薦】,也可【評論】留下您的問題或建議與我交流。 你的支援是我不斷創作和分享的不竭動力!

作者:『聖傑』

出處:http://www.cnblogs.com/sheng-jie/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連結,否則保留追究法律責任的權利。