记一次 .NET 某珠宝公司内部管理系统 内存暴涨分析

发布时间:2026-09-16 09:32  浏览量:2

好久都没写文章了,看现在各个社区大多都是AI写的文章,劣币驱除良币,文字这块算是沦陷了,没多少 passion to continue,但遇到一些经典的还是会人肉堆一堆,这篇我们就来分析一个内存暴涨的例子,这是一个朋友在微信上找到我的,有一个linux上的.NET程序,内存在一直暴涨,发现非托管内存占用不少,让我帮忙看下咋回事。既然是Linux上的dump,用传统的!address -summary就不靠谱了,这里就需要用!maddress

0:000> !maddress -summary ++ | Memory Type | Count | Size | Size (bytes) | ++ | GCHeap | 24 | 1.19gb | 1,282,744,320 | | PAGE_READWRITE | 173 | 1016.34mb | 1,065,705,472 | | Stack | 86 | 693.90mb | 727,609,344 | | Image | 1,129 | 147.84mb | 155,026,432 | | HighFrequencyHeap | 411 | 25.66mb | 26,906,624 | | LowFrequencyHeap | 272 | 18.70mb | 19,607,552 | | LoaderCodeHeap | 13 | 17.00mb | 17,825,792 | | HostCodeHeap | 10 | 1.16mb | 1,220,608 | | ResolveHeap | 1 | 348.00kb | 356,352 | | PAGE_READONLY | 113 | 239.00kb | 244,736 | | DispatchHeap | 1 | 196.00kb | 200,704 | | IndirectionCellHeap | 3 | 152.00kb | 155,648 | | LookupHeap | 3 | 144.00kb | 147,456 | | StubHeap | 3 | 140.00kb | 143,360 | | PAGE_EXECUTE_WRITECOPY | 5 | 116.00kb | 118,784 | | CacheEntryHeap | 2 | 100.00kb | 102,400 | | PAGE_EXECUTE_READ | 2 | 8.00kb | 8,192 | ++ | [TOTAL] | 2,251 | 3.07gb | 3,298,123,776 | ++

从卦中可以看到,程序总计吃了3.07G,其中GCHeap和PAGE_READWRITE吃的差不多,看起来不大乐观,要先追踪PAGE_READWRITE的调用栈,在 linux 上不是那么容易的。

2. 从托管堆入手

接下来怎么办呢?先死马当做活马医,因为毕竟是托管程序,很多非托管内存的root都和托管堆对象有关,本着这个思想,先用!dumpheap -stat观察下托管堆看看。

0:000> !dumpheap -statStatistics: MT Count TotalSize Class Name...7f59a044dc88 3,765331,320 System.Data.SqlClient.SNI.SNIMarsConnection7f59a02a5e88 11,295903,600 System.Collections.Generic.Dictionary7f59a02a2238 11,2954,608,360 System.Data.SqlClient.SNI.TdsParserStateObjectManaged...7f59a029f7b8 3,7657,801,080 System.Data.SqlClient.SessionStateRecord7f5999a9a740 3,8827,817,152 System.Byte7f59a044b738 139,54425,676,096 System.Data.SqlClient._SqlMetaData7f599b869e78 145,25627,889,152 xxx.SaleDeptInfo7f59993fd2e0 2,119,130100,150,180 System.String556ba0740670 8,748341,803,536 Free7f59999cd8b8 103,573412,502,480 System.ByteTotal 3,599,633 objects, 998,240,010 bytes

仔细观察卦中的数据,很容易发现SqlClient相关的对象的数量有点多,尤其是SNIMarsConnection高达 3765 个,这个是不正常的,你可以简单理解底层开了 3765 个 connection 链接,每个链接都会吃一点非托管资源,所以非托管内存就这样上去了。

3. SNIMarsConnection 是啥

Mars 全称multiple active result sets,主要是解决 command 下的多 reader 问题,这里大家可以问下大模型,具体就不说了,接下来就从入手,看看它的root情况。

0:000> !dumpheap -mt 7f59a044dc88 Address MT Size ...7f5804d31938 7f59a044dc88 887f5804d6c950 7f59a044dc88 887f5804d99930 7f59a044dc88 887f5804dcf330 7f59a044dc88 88Statistics: MT Count TotalSize Class Name7f59a044dc88 3,765331,320 System.Data.SqlClient.SNI.SNIMarsConnectionTotal 3,765 objects, 331,320 bytes0:000> !gcroot 7f5804dcf330 HandleTable:00007f5a113510f8 (strong handle) -> 7f5943fff018 System.Object -> 7f5684029f90 System.Data.SqlClient.SNI.SNIMarsManager (static variable: System.Data.SqlClient.SNI.SNILoadHandle.SingletonInstance) -> 7f5684029fa8 System.Collections.Concurrent.ConcurrentDictionary -> 7f558679f550 System.Collections.Concurrent.ConcurrentDictionary

从卦中看,原来这 3765 个 connection 都是被 SNIMarsManager 所持有,接下来就是扒它的源码,截图如下:

image

源码是有了,但貌似useless,接下来怎么办呢?全网求助啦。

4. 网络求助

很快就找到了一篇文章:https://joshthecoder.com/2021/10/26/preventable-mars-connection-leaks.html ,作者很详细的讲解了MARS的来龙去脉,主要原因就是用户开了MultipleActiveResultSets=True特性之后,后续使用 connection 套件的时候没有及时的dispose/close引发的问题,感兴趣的朋友可以去看看。为了方便验证,我写了一个小脚本,去看看 connectionstring 是不是带有,这里也给大家安全提醒,如果这些信息丢给大模型,可能就是数据出境了,风险你懂的。。。

function invokeScript {var output = exec("!dumpheap -mt 7f599fe3c538").Skip(1);for (var line of output) {if (!line) break;var addr = line.split(' ')[0].trim;var connection = exec("du /c100 poi("+addr+"+0x38)+0xc").First; log("addr="+addr+" connection="+connection); }}

image 所以这个问题的quick fix也很简单,去掉就可以了。这个方案可以这么quick fix,但如果要治根的话需要优化代码,但这个改动不是那么快速,后来又想想感觉微软的底层做的也不是那么好,应该要有类似的timer机制来自动化压缩和增长,奔着这个思路在网上找找,还真给找到了,参考:https://github.com/dotnet/runtime/issues/22949

image

从卦上可以清晰的看到,升级下 SQLClient 的版本也是可以的。

到这里所有的来龙去脉都搞清楚了,做好两件事情即可。

MultipleActiveResultSets=True可以快速应急。 升级 SQLClient版本治根,当然也可以自己优化代码。

这次生产事故本质上来说是微软官方库的bug导致的问题,有时候追到这里也是挺无奈的。