记一次 .NET 某低代码开发框架 内存暴涨分析

一:背景

1. 讲故事

微信里有一位朋友找到我,说他们公司的程序存在内存暴涨问题,自己分析了下没有找到原因,让我看下怎么回事?由于大家都有dump分析基础,所以交流互通上还是很顺利的,接下来就是上dump分析啦。

二:内存暴涨分析

1. 为什么会内存暴涨

先还是老套路,用 !address -summary 观察下内存分布情况,输出如下:

C# 复制代码
0:000> !address -summary

--- Usage Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
Free                                    363     7dfd`e87c7000 ( 125.992 TB)           98.43%
<unknown>                              9276      201`e5858000 (   2.007 TB)  99.96%    1.57%
Heap                                     65        0`2547f000 ( 596.496 MB)   0.03%    0.00%
Image                                  1855        0`09d35000 ( 157.207 MB)   0.01%    0.00%
Stack                                    93        0`02c00000 (  44.000 MB)   0.00%    0.00%
Other                                     9        0`001de000 (   1.867 MB)   0.00%    0.00%
TEB                                      31        0`0003e000 ( 248.000 kB)   0.00%    0.00%
PEB                                       1        0`00001000 (   4.000 kB)   0.00%    0.00%

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                363     7dfd`e87c7000 ( 125.992 TB)           98.43%
MEM_RESERVE                             690      201`2b6d4000 (   2.005 TB)  99.82%    1.57%
MEM_COMMIT                            10640        0`ec155000 (   3.689 GB)   0.18%    0.00%

从卦中可以看到,总计 3.6G 的总提交内存,看样子都落到了 Unk 区域,最好是托管层吃掉了,否则就麻烦了,接下来使用 !dumpheap -stat 观察,输出如下:

C# 复制代码
0:000> !dumpheap -stat
Statistics:
          MT      Count     TotalSize Class Name
...
0179c7715cb0  1,847,901   451,265,880 Free
7ffc6e0a2888          2   536,870,960 System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
7ffc6e0a2260 60,873,978 1,460,975,472 System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>
Total 63,333,893 objects, 2,494,520,292 bytes

从卦中可以看到程序中有 6087w 个弱引用,接下来使用 !dumpheap -mt 7ffc6e0a2260 观察下列表详情,然后用 !gcroot 观察其引用根,参考如下:

C# 复制代码
0:000> !dumpheap -mt 7ffc6e0a2260
         Address               MT           Size
    017988001000     7ffc6e0a2260             24 
    017988001018     7ffc6e0a2260             24 
    017988001030     7ffc6e0a2260             24 
    017988001048     7ffc6e0a2260             24 
    017988001060     7ffc6e0a2260             24 
    017988001078     7ffc6e0a2260             24 
    017988001090     7ffc6e0a2260             24 
    0179880010a8     7ffc6e0a2260             24 
    ...
    017a405f1020     7ffc6e0a2260             24 

0:000> !gcroot   0179880010a8  
Caching GC roots, this may take a while.
Subsequent runs of this command will be faster.

等了20多分钟都没有出来结果,可能 6kw 的根纵横交错让windbg不堪重负,没有就没撤了,使用内存搜索法寻找上级所属对象。这里就选择 017a405f1020 对象来开刀。

C# 复制代码
0:000> !dumpobj /d 17a405f1020
Name:        System.WeakReference`1[[Microsoft.Extensions.DependencyInjection.ServiceProvider, Microsoft.Extensions.DependencyInjection]][]
MethodTable: 00007ffc6e0a2888
EEClass:     00007ffc6dbeb4f8
Tracked Type: false
Size:        536870936(0x20000018) bytes
Array:       Rank 1, Number of elements 67108864, Type CLASS (Print Array)
Fields:
None

0:000> s-q 0 L?0xffffffffffffffff 17a405f1020
00000179`c95861d0  0000017a`405f1020 03a0dcfa`03a0dcfa

0:000> !lno 0000017a`405f1020
Before:       017a405f1000 32 (0x20)                        Free
Current:      017a405f1020 24 (0x18)                        System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
Error Detected: Object 17a405f1020 has a bad member at offset 12054c00: ??? [verify heap]
Could not find object after 17a405f1020
Heap local consistency not confirmed.

0:000> !lno 00000179`c95861d0
Before:       0179c95861c8 32 (0x20)                        System.Collections.Generic.List<System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>>
Next:         0179c95861e8 24 (0x18)                        System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>[]
Heap local consistency confirmed.

0:000> !dumpobj /d 179c95861c8
Name:        System.Collections.Generic.List`1[[System.WeakReference`1[[Microsoft.Extensions.DependencyInjection.ServiceProvider, Microsoft.Extensions.DependencyInjection]], System.Private.CoreLib]]
MethodTable: 00007ffc6e0a2340
EEClass:     00007ffc6dce0000
Tracked Type: false
Size:        32(0x20) bytes
File:        D:\xxx\A_api\System.Private.CoreLib.dll
Fields:
              MT    Field   Offset                 Type VT     Attr            Value Name
00007ffc6de328f0  400209f        8     System.__Canon[]  0 instance 0000017a405f1020 _items
00007ffc6dc894b0  40020a0       10         System.Int32  1 instance         60873978 _size
00007ffc6dc894b0  40020a1       14         System.Int32  1 instance         60873978 _version
00007ffc6de328f0  40020a2        8     System.__Canon[]  0   static dynamic statics NYI                 s_emptyArray

0:000> s-q 0 L?0xffffffffffffffff 179c95861c8
00000179`c77571d8  00000179`c95861c8 00000000`00000000
00000179`c95861b8  00000179`c95861c8 0800004e`00000000

0:000> !lno 00000179`c77571d8
Failed to find the segment of the managed heap where the object 179c77571d8 resides

0:000> !lno 00000179`c95861b8
Before:       0179c9586108 192 (0xc0)                       Microsoft.Extensions.DependencyInjection.DependencyInjectionEventSource
Next:         0179c95861c8 32 (0x20)                        System.Collections.Generic.List<System.WeakReference<Microsoft.Extensions.DependencyInjection.ServiceProvider>>
Heap local consistency confirmed.

根据卦中的图和输出,终于找到了原来是 DependencyInjectionEventSource._providers 承担了所有,接下来的关注点就来到了 DependencyInjectionEventSource

2. xxxEventSource 是什么

从名字上看和 ETW 事件有关,接下来用 !eeversion 观察 .net 版本,寻找其对应的C#源代码。

C# 复制代码
0:000> !eeversion
6.0.3624.51421 free
6,0,3624,51421 @Commit: f1dd57165bfd91875761329ac3a8b17f6606ad18
Workstation mode
SOS Version: 9.0.13.2701 retail build

从上面的源代码看,其实也看不出来个所以,毕竟底层的架构我不熟悉,本着我不是第一个吃螃蟹的人,所以拿关键词在网上索一下,果然 stephentoub 大佬在去年4月份就发现了这个问题,在 .net10 中做了修复,看描述是一个优化级的bug,官方链接:https://github.com/dotnet/runtime/issues/114599 截图如下:

修改后的代码如下,果然加了很多的业务逻辑来处理。

C# 复制代码
        [NonEvent]
        public void ServiceProviderBuilt(ServiceProvider provider)
        {
            lock (_providers)
            {
                int providersCount = _providers.Count;
                if (providersCount > 0 &&
                    (_survivingProvidersCount is int spc ? (uint)providersCount >= 2 * (uint)spc : providersCount == _providers.Capacity))
                {
                    _providers.RemoveAll(static p => !p.TryGetTarget(out _));
                    _survivingProvidersCount = _providers.Count;
                }

                _providers.Add(new WeakReference<ServiceProvider>(provider));
            }

            WriteServiceProviderBuilt(provider);
        }

从官方描述来看,就是有人创建了 scope,但后续没有调用 dispose 方法来及时释放,导致框架中的 WeakReference 引用滞留,引发内存暴涨,可以说两者都有责任吧。

解决办法很简单,两种方式:

  1. 检查代码里写 BuildServiceProvider 的地方没有即时的 Dispose。
  2. 升级到 .NET10 ,这是最简单粗暴的方法。

把结论告诉朋友后,朋友终于在2天后给我反馈了好消息,好心情溢于言表!

三:总结

dump之旅是一个修理工不断自我修炼的过程,必须学会在绝望中寻找希望的能力。