記一次 .NET 某HIS系統後端服務 記憶體洩漏分析

一線碼農發表於2021-05-21

一:背景

1. 講故事

前天那位 his 老哥又來找我了,上次因為CPU爆高的問題我給解決了,看樣子對我挺信任的,這次另一個程式又遇到記憶體洩漏,希望我幫忙診斷下。

其實這位老哥技術還是很不錯的,他既然能給我dump,那真的是遇到很棘手的疑難雜症了???,我得做好心理準備???,溝通下來大概就是程式的記憶體會緩慢膨脹,直到自毀,問題就是這麼一個問題,接下來祭出我的看家工具 windbg。

二: windbg 分析

1. 到底哪裡洩漏了?

我在之前很多篇文章中都說過,遇到這種記憶體洩漏,首先就要排查到底是 託管堆 還是 非託管堆 的問題 ?如果是後者,大多數情況只能舉手投降,因為這裡面水太深了。。。 別看那些案例用 AllocHGlobal 方法分配非託管記憶體,然後用 !heap 去找的小兒科,現實情況比這種要複雜的多。。。

接下來先用 !address -summary 看一下當前程式的提交記憶體。


0:000> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                    345     7dfd`ca3ca000 ( 125.991 TB)           98.43%
<unknown>                             37399      201`54dbf000 (   2.005 TB)  99.83%    1.57%
Heap                                  29887        0`d179b000 (   3.273 GB)   0.16%    0.00%
Image                                  1312        0`0861b000 ( 134.105 MB)   0.01%    0.00%
Stack                                   228        0`06e40000 ( 110.250 MB)   0.01%    0.00%
Other                                    10        0`001d8000 (   1.844 MB)   0.00%    0.00%
TEB                                      76        0`00098000 ( 608.000 kB)   0.00%    0.00%
PEB                                       1        0`00001000 (   4.000 kB)   0.00%    0.00%

--- Type Summary (for busy) ------ RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_MAPPED                              352      200`00a40000 (   2.000 TB)  99.57%    1.56%
MEM_PRIVATE                           67249        2`2cbcb000 (   8.699 GB)   0.42%    0.01%
MEM_IMAGE                              1312        0`0861b000 ( 134.105 MB)   0.01%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                345     7dfd`ca3ca000 ( 125.991 TB)           98.43%
MEM_RESERVE                           11805      200`22ae8000 (   2.001 TB)  99.60%    1.56%
MEM_COMMIT                            57108        2`1313e000 (   8.298 GB)   0.40%    0.01%

從卦象上看, 程式提交記憶體 MEM_COMMIT = 8.2G, 然後我們看下託管堆大小,使用 !eeheap -gc 命令。


0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x0000027795928060
generation 1 starts at 0x000002779572F0D0
generation 2 starts at 0x000002763DCE1000

Total Size:              Size: 0xcd28c510 (3442001168) bytes.
------------------------------
GC Heap Size:    Size: 0xcd28c510 (3442001168) bytes.

從最後一行可以看出,當前的GC堆 Size= 3442001168 /1024/1024/1024 =3.2G,也就是說大概: 8.2G - 3.2G = 5G 的記憶體丟掉了。。。尼瑪,典型的 非託管記憶體洩漏,真的是哪壺不開提哪壺,這下可能真的要栽了。。。

2. 尋找非託管記憶體洩漏

除了 GC 堆,程式裡面還有一個叫做 loader 堆,這裡面東西就多了,有高頻堆,低頻堆,Stub堆,JIT堆 等等,存放著和 AppDomain,Module,方法描述符,方法表,EEClass 等相關資訊,從經驗來說,這個 loader 堆是考察 非託管洩漏 優先考慮的地方,要想檢視,可使用 !eeheap -loader 命令。


0:000> !eeheap -loader
...
Module 00007ffe2b1b6ca8: Size: 0x0 (0) bytes.
Module 00007ffe2b1b7e80: Size: 0x0 (0) bytes.
Module 00007ffe2b1b9058: Size: 0x0 (0) bytes.
Module 00007ffe2b1ba230: Size: 0x0 (0) bytes.
Module 00007ffe2b1bb408: Size: 0x0 (0) bytes.
Module 00007ffe2b1bc280: Size: 0x0 (0) bytes.
Module 00007ffe2b1bd458: Size: 0x0 (0) bytes.
Module 00007ffe2b1be630: Size: 0x0 (0) bytes.
Module 00007ffe2b1bf808: Size: 0x0 (0) bytes.
Module 00007ffe2b1f0a50: Size: 0x0 (0) bytes.
Module 00007ffe2b1f1c28: Size: 0x0 (0) bytes.
Module 00007ffe2b1f2aa0: Size: 0x0 (0) bytes.
Total size:      Size: 0x0 (0) bytes.
--------------------------------------
Total LoaderHeap size:   Size: 0xc0fb9000 (3237711872) bytes total, 0x5818000 (92372992) bytes wasted.

這命令不輸還好,一輸嚇一跳,windbg 介面刷了好幾分鐘才停下來。。。 從輸出中可以得到兩點資訊:

  • loader堆 總共佔用: 3237711872 /1024/1024/1024 = 3.01G

  • 有非常多的 module 產生,我估計有幾萬個。。。

為了滿足好奇心,我決定寫一個小指令碼看看到底有多少個 module ???

我去,module居然有19w之多,難怪佔用了 3 個多G,感覺離真相不遠了,接下來的問題是這些module是什麼,從哪裡來???

3. 尋找 module 的源頭

要想尋找源頭,大家可以仔細想一想, module 的巢狀關係應該是: Module -> Assembly -> Appdomain ,所以查 AppDomain 或許能給我們更多的資訊,接下來使用 !DumpDomain 匯出當前程式的所有應用程式域,又是刷刷刷的幾分鐘,哎。。。 截圖如下:

從圖中可以看出有大量的 Dynamic 型別的程式集,你肯定想問這是什麼意思? 對,這就是程式碼動態建立的程式集,居然高達 19w 。。。接下來要解決的一個問題是:這些 Assembly 是怎麼建立出來的???

4. 匯出 module 內容

老讀者應該知道我是怎麼從 module 中匯出問題程式碼的,對,就是尋找 module 的 startaddress,這裡我就挑選其中一個module:00007ffe2b1f2aa0。


2:2:152> !dumpmodule 00007ffe2b1f2aa0
Name: Unknown Module
Attributes:              Reflection SupportsUpdateableMethods IsDynamic IsInMemory 
Assembly:                000002776c1d8470
BaseAddress:             0000000000000000
PEFile:                  000002776C1D8BF0
ModuleId:                00007FFE2B1F2EB8
ModuleIndex:             00000000000177CF
LoaderHeap:              0000000000000000
TypeDefToMethodTableMap: 00007FFE2B1EE8C0
TypeRefToMethodTableMap: 00007FFE2B1EE8E8
MethodDefToDescMap:      00007FFE2B1EE910
FieldDefToDescMap:       00007FFE2B1EE960
MemberRefToDescMap:      0000000000000000
FileReferencesMap:       00007FFE2B1EEA00
AssemblyReferencesMap:   00007FFE2B1EEA28

我去,BaseAddress 居然沒有地址,真倒黴,這也就是說該 module 你是無法匯出的,想想也對,畢竟是動態生成的,可能寫程式碼的人都搞不清楚module中是什麼?難道真的就沒有辦法了嗎? 可俗話說得好,天無絕人之路???,在 !dumpmodule 命令中有一個 mt (methodtable) 引數,用來顯示當前module中都有哪些型別,這就是重大線索。


||2:2:152> !dumpmodule -mt 00007ffe2b1f2aa0 
Name: Unknown Module
Attributes:              Reflection SupportsUpdateableMethods IsDynamic IsInMemory 
Assembly:                000002776c1d8470

Types defined in this module

              MT          TypeDef Name
------------------------------------------------------------------------------
00007ffe2b1f3168 0x02000002 <Unloaded Type>
00007ffe2b1f2f60 0x02000003 <Unloaded Type>

Types referenced in this module

              MT            TypeRef Name
------------------------------------------------------------------------------
00007ffdb9f70af0 0x02000001 System.Object
00007ffdbaed3730 0x02000002 Castle.DynamicProxy.IProxyTargetAccessor
00007ffdbaec8f98 0x02000003 Castle.DynamicProxy.ProxyGenerationOptions
00007ffdbaec7fe8 0x02000004 Castle.DynamicProxy.IInterceptor

可以看到module中定義了兩個 type,都有其方法表地址,接下來通過 mt 來換取 md (方法描述符) 來得到最後module內容。

到這裡終於就搞清楚了,原來這位老哥是利用 Castle 做了一個 AOP 的功能,應該是沒有正確的使用 AOP ,導致生成了 19w + 的動態程式集,難怪最終會把記憶體給弄爆掉。。。 根子總算找到了,接下來如何去修改呢???

5. 修改 Castle AOP 問題程式碼

這下可把我難住了,畢竟我真的是沒玩過 Castle ???,不過老規矩,到 bing 上看看可有 天涯淪落人,嘿嘿,還真有 Castle AOP 導致記憶體洩漏的文章:Castle Windsor Interceptor memory leak ,解決辦法也提供了,截圖如下:

趕緊把這篇連結丟給老哥,我感覺也只能幫他到這裡了,剩下的只能看造化。

三:總結

真的是造化弄人,老哥以迅雷不及掩耳之勢就給搞定了,當天晚上就已完成自測上線。


我趕緊追問老哥是怎麼改的???,老哥也不惜把原始碼放出來了,果然按照老外的建議將 ProxyGenerator 設定成 static 就搞定了。。。否則一個new一個assembly,再看看改之前的程式碼,截圖如下:

搞定了這兩個難啃的問題,感覺是不是要發一個小獎盃給我呢????

更多高質量乾貨:參見我的 GitHub: dotnetfly

圖片名稱

相關文章