• 记一次 .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,其中 GCHeap 和 PAGE_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
    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 
              -> 7f558679f550     System.Collections.Concurrent.ConcurrentDictionary+Tables 
              -> 7f558678cd38     System.Collections.Concurrent.ConcurrentDictionary+Node[] 
              -> 7f5804dcf480     System.Collections.Concurrent.ConcurrentDictionary+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
        -----
          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导致的问题,有时候追到这里也是挺无奈的。
    图片名称

  • 相关阅读:
    redis集群
    nginx负载均衡
    每天一道算法题(八)——找出字符串中无重复字符的最长子串
    Sentinel的流控与熔断降级规则详解
    数据分享|R语言分析上海空气质量指数数据:kmean聚类、层次聚类、时间序列分析:arima模型、指数平滑法...
    谷粒学苑_第十天
    CMake与makefile的区别
    大话STL第六期——map/multimap
    【C++】C / C++ 内存管理
    jQuery 的实现原理
  • 原文地址:https://www.cnblogs.com/huangxincheng/p/22983141