记一次 .NET 某HIS系统后端服务 内存泄漏分析

  • A+
所属分类:.NET技术
摘要

前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下。


一:背景

1. 讲故事

前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下。

记一次 .NET 某HIS系统后端服务 内存泄漏分析

其实这位老哥技术还是很不错的,他既然能给我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 ???

记一次 .NET 某HIS系统后端服务 内存泄漏分析

我去,module居然有19w之多,难怪占用了 3 个多G,感觉离真相不远了,接下来的问题是这些module是什么,从哪里来???

3. 寻找 module 的源头

要想寻找源头,大家可以仔细想一想, module 的嵌套关系应该是: Module -> Assembly -> Appdomain ,所以查 AppDomain 或许能给我们更多的信息,接下来使用 !DumpDomain 导出当前进程的所有应用程序域,又是刷刷刷的几分钟,哎。。。 截图如下:

记一次 .NET 某HIS系统后端服务 内存泄漏分析

从图中可以看出有大量的 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内容。

记一次 .NET 某HIS系统后端服务 内存泄漏分析

到这里终于就搞清楚了,原来这位老哥是利用 Castle 做了一个 AOP 的功能,应该是没有正确的使用 AOP ,导致生成了 19w + 的动态程序集,难怪最终会把内存给弄爆掉。。。 根子总算找到了,接下来如何去修改呢???

5. 修改 Castle AOP 问题代码

这下可把我难住了,毕竟我真的是没玩过 Castle ???,不过老规矩,到 bing 上看看可有 天涯沦落人,嘿嘿,还真有 Castle AOP 导致内存泄漏的文章:Castle Windsor Interceptor memory leak ,解决办法也提供了,截图如下:

记一次 .NET 某HIS系统后端服务 内存泄漏分析

赶紧把这篇链接丢给老哥,我感觉也只能帮他到这里了,剩下的只能看造化。

三:总结

真的是造化弄人,老哥以迅雷不及掩耳之势就给搞定了,当天晚上就已完成自测上线。

记一次 .NET 某HIS系统后端服务 内存泄漏分析
记一次 .NET 某HIS系统后端服务 内存泄漏分析

我赶紧追问老哥是怎么改的???,老哥也不惜把源码放出来了,果然按照老外的建议将 ProxyGenerator 设置成 static 就搞定了。。。否则一个new一个assembly,再看看改之前的代码,截图如下:

记一次 .NET 某HIS系统后端服务 内存泄漏分析

搞定了这两个难啃的问题,感觉是不是要发一个小奖杯给我呢????

更多高质量干货:参见我的 GitHub: dotnetfly

记一次 .NET 某HIS系统后端服务 内存泄漏分析