性能优化2026年9月15日· 约 16 分钟

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

#内存分析#.NET#内存管理
Twitter 微博

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

一:背景#

1. 讲故事#

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

二:内存暴涨分析#

1. 为什么会暴涨#

既然是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,其中 GCHeapPAGE_READWRITE 吃的差不多,看起来不大乐观,要先追踪 PAGE_READWRITE 的调用栈,在 linux 上不是那么容易的。

2. 从托管堆入手#

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


0:000> !dumpheap -stat
Statistics:
          MT     Count   TotalSize Class Name
...
7f59a044dc88     3,765     331,320 System.Data.SqlClient.SNI.SNIMarsConnection
7f59a02a5e88    11,295     903,600 System.Collections.Generic.Dictionary<System.Data.SqlClient.SNI.SNIPacket, System.Data.SqlClient.SNI.SNIPacket>
7f59a02a2238    11,295   4,608,360 System.Data.SqlClient.SNI.TdsParserStateObjectManaged
...
7f59a029f7b8     3,765   7,801,080 System.Data.SqlClient.SessionStateRecord[]
7f5999a9a740     3,882   7,817,152 System.Byte[][]
7f59a044b738   139,544  25,676,096 System.Data.SqlClient._SqlMetaData
7f599b869e78   145,256  27,889,152 xxx.SaleDeptInfo
7f59993fd2e0 2,119,130 100,150,180 System.String
556ba0740670     8,748 341,803,536 Free
7f59999cd8b8   103,573 412,502,480 System.Byte[]
Total 3,599,633 objects, 998,240,010 bytes

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

3. SNIMarsConnection 是啥#

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


0:000> !dumpheap -mt 7f59a044dc88
         Address               MT           Size
         ...
    7f5804d31938     7f59a044dc88             88 
    7f5804d6c950     7f59a044dc88             88 
    7f5804d99930     7f59a044dc88             88 
    7f5804dcf330     7f59a044dc88             88 

Statistics: MT Count TotalSize Class Name 7f59a044dc88 3,765 331,320 System.Data.SqlClient.SNI.SNIMarsConnection Total 3,765 objects, 331,320 bytes

0: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<System.Data.SqlClient.SNI.SNIHandle, System.Data.SqlClient.SNI.SNIMarsConnection> -> 7f558679f550 System.Collections.Concurrent.ConcurrentDictionary<System.Data.SqlClient.SNI.SNIHandle, System.Data.SqlClient.SNI.SNIMarsConnection>+Tables -> 7f558678cd38 System.Collections.Concurrent.ConcurrentDictionary<System.Data.SqlClient.SNI.SNIHandle, System.Data.SqlClient.SNI.SNIMarsConnection>+Node[] -> 7f5804dcf480 System.Collections.Concurrent.ConcurrentDictionary<System.Data.SqlClient.SNI.SNIHandle, System.Data.SqlClient.SNI.SNIMarsConnection>+Node -> 7f5804dcf330 System.Data.SqlClient.SNI.SNIMarsConnection

Found 1 unique roots.

0:000> !do 7f5684029f90 Name: System.Data.SqlClient.SNI.SNIMarsManager MethodTable: 00007f59a044da00 EEClass: 00007f59a043ecc8 Tracked Type: false Size: 24(0x18) bytes File: /xxx/System.Data.SqlClient.dll Fields: MT Field Offset Type VT Attr Value Name 00007f59a044dd18 400067e 8 ....Data.SqlClient]] 0 instance 00007f5684029fa8 _connections 00007f59a044da00 400067d 3f0 ...NI.SNIMarsManager 0 static 00007f5684029f90 Singleton 0:000> !ext dcd 00007f5684029fa8 System.Collections.Concurrent.ConcurrentDictionary<System.Data.SqlClient.SNI.SNIHandle, System.Data.SqlClient.SNI.SNIMarsConnection> ----- Key: dumpobj 00007f5587a88430 Value: dumpobj 00007f5686cb0988


3765 items

0:000> !objsize 7f5684029f90 -summary Objects which 7f5684029f90 (System.Data.SqlClient.SNI.SNIMarsManager) transitively keep alive: ... Total 943,803 objects, 431,256,544 bytes

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

image

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

4. 网络求助#

很快就找到了一篇文章:https://joshthecoder.com/2021/10/26/preventable-mars-connection-leaks.html ,作者很详细的讲解了 MARS 的来龙去脉,主要原因就是用户开了 MultipleActiveResultSets=True 特性之后,后续使用 connection 套件的时候没有及时的 dispose/close 引发的问题,感兴趣的朋友可以去看看。

为了方便验证,我写了一个小脚本,去看看 connectionstring 是不是带有 MultipleActiveResultSets=True,这里也给大家安全提醒,如果这些信息丢给大模型,可能就是数据出境了,风险你懂的。。。


function invokeScript() {
var output = exec(class="hljs-string">"!dumpheap -mt 7f599fe3c538").Skip(class="hljs-number">1);

for (var line of output) {
    if (!line) break;

    var addr = line.split(class="hljs-string">'     ')[class="hljs-number">0].trim();

    var connection = exec(class="hljs-string">"du /c100 poi("+addr+class="hljs-string">"+0x38)+0xc").First();

    log(class="hljs-string">"addr="+addr+class="hljs-string">" connection="+connection);
}

}

image

所以这个问题的quick fix也很简单,去掉 MultipleActiveResultSets=True 就可以了。

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

image

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

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

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

三:总结#

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


原文链接:https://www.cnblogs.com/huangxincheng/p/22983141

评论

© 2026 松岛川树