惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
有赞技术团队
有赞技术团队
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
V
Visual Studio Blog
博客园_首页
大猫的无限游戏
大猫的无限游戏
Y
Y Combinator Blog
博客园 - Franky
Vercel News
Vercel News
H
Hackread – Cybersecurity News, Data Breaches, AI and More
U
Unit 42
IT之家
IT之家
Last Week in AI
Last Week in AI
腾讯CDC
Martin Fowler
Martin Fowler
S
SegmentFault 最新的问题
量子位
I
InfoQ
T
The Blog of Author Tim Ferriss
The Cloudflare Blog
MyScale Blog
MyScale Blog
C
Check Point Blog

博客园 - 一线码农

记一次 .NET 某智慧医保云服务Linux 非托管泄露分析 .NET 高级调试技术:超越基础 Dump 分析 .NET 生产环境调试实战指南 记一次 .NET 某光电成像后端解析系统 内存暴涨分析 记一次 .NET 某电力后台监控系统 内存暴涨分析 记一次 .NET 某注塑模具系统 CPU爆高分析 记一次 .NET 某集群管理软件 内存暴涨分析 记一次 .NET 某智慧工厂视觉程序 崩溃分析 记一次 .NET 某低代码开发框架 内存暴涨分析 记一次 .NET 某MES上位机拍照系统 内存暴涨分析 记一次 .NET 某RFID标签打印客户端 崩溃分析 对 .NET FileSystemWatcher引发内存碎片化的 反思 DotMemory系列:5. 如何实现自动化抓取和应用自托管 DotMemory系列:4. 如何分析进程的转储文件 DotMemory系列:3. 堆碎片化引发的内存暴涨分析 DotMemory系列:2. 事件泄露引发的内存暴涨分析 DotMemory系列:1. 终结队列积压引发的内存暴涨分析 记一次 .NET 某理财管理客户端 OOM溢出分析 记一次 .NET 某医联体管理系统 崩溃分析 记一次 .NET 某光放测试系统 崩溃分析 记一次 .NET 某药品缺陷高速检测系统 卡慢分析 聊一聊 .NET超高内存故障分析方法 的反思 记一次 .NET 某企业ECM内容管理系统 内存暴涨分析 记一次 .NET 某跨境物流系统 内存暴涨分析 记一次 .NET 某中医药附属医院门诊系统 崩溃分析 聊一聊 .NET 中的 CompositeChangeToken 聊一聊 .NET 中的 CancellationTokenSource 记一次 .NET 某CRM物流行业管理系统 崩溃分析
记一次 .NET 某珠宝公司内部管理系统 内存暴涨分析
一线码农 · 2026-09-15 · via 博客园 - 一线码农

一:背景

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("!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也很简单,去掉 MultipleActiveResultSets=True 就可以了。

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

image

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

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

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

三:总结

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