记一次 .NET 某风控管理系统 内存泄漏分析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了记一次 .NET 某风控管理系统 内存泄漏分析相关的知识,希望对你有一定的参考价值。

一:背景

1. 讲故事

上个月中旬,星球里的一位朋友在微信找我,说他的程序跑着跑着内存会不断的缓慢增长并无法释放,寻求如何解决 ?

得,看样子星球还得好好弄!!!

记一次 .NET 某智能服装智造系统 内存泄漏分析

一:背景

1. 讲故事

上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下:

朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧。

二:Windbg 分析

1. 到底是哪一方面的泄漏

根据朋友描述,程序运行一段时间后,内存就炸了,应该没造成人员伤亡,不然也不会跟我wx聊天了,这里可以用 .time 看看当前的 process 跑了多久。


0:000> .time
Debug session time: Thu Oct 21 14:54:39.000 2021 (UTC + 8:00)
System Uptime: 6 days 4:37:27.851
Process Uptime: 0 days 0:40:14.000
  Kernel time: 0 days 0:01:55.000
  User time: 0 days 0:07:33.000

看的出来,这个 dump 是在程序跑了 40min 之后抓的,接下来我们比较一下 process 的内存和 gc堆 占比, 看看到底是哪一块的泄漏。


0:000> !address -summary

--- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE                                327     7dfc`c665a000 ( 125.987 TB)           98.43%
MEM_RESERVE                             481      201`e91a2000 (   2.007 TB)  99.74%    1.57%
MEM_COMMIT                             2307        1`507f4000 (   5.258 GB)   0.26%    0.00%

0:000> !eeheap -gc
Number of GC Heaps: 2
------------------------------

GC Allocated Heap Size:    Size: 0x139923528 (5260850472) bytes.
GC Committed Heap Size:    Size: 0x13bf23000 (5300695040) bytes.

从卦中可轻松得知, 这是一个完完全全的托管堆内存泄漏。

2. 到底是什么占用了如此大的内存

知道是 托管层 的泄漏,感觉一下子就幸福起来了,接下来用 !dumpheap -stat 看看有没有什么大对象可挖。


0:000> !dumpheap -stat
Statistics:
              MT    Count    TotalSize Class Name
00007ffdeb1fc400  5362921    128710104 xxxBLLs.xxx.BundleBiz+<>c__DisplayClass20_0
00007ffdeaeff140  5362929    171613728 System.Collections.Generic.List`1[[xxx.xxx, xxx]]
00007ffdeaeff640  5362957    171615272 xxx.BLLs.Plan.Dto.xxx[]
00007ffde8171e18 16146362    841456072 System.String
00007ffdeb210098  5362921   1415811144 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
00007ffdea9ca260  5362921   2359685240 xxx.Bundle             

从输出看,内存主要被 xxx.BundleAsyncTaskMethodBuilder 两大类对象给吃掉了,数量都高达 536w,这里有一个非常有意思的地方,如果你了解异步,我相信你一看就能看出 AsyncTaskMethodBuilder + VoidTaskResult 是干嘛的,按照经验,这位朋友应该误入了 异步无限递归 ,那怎么去挖呢? 接着往下看。

3. 寻找问题代码

看到上面的 xxx.BundleBiz+<DistributionBundle>d__20 了吗? 这个正是异步操作所涉及到的类和方法,接下来用 ILSpy 反射出 BundleBiz 下的匿名类 <DistributionBundle>d__20 , 如下图所示:

虽然找到了源码,但代码是 ILSpy 反编译出来的异步状态机,接下来的一个问题是,如何根据状态机代码反向寻找到 await ,async 代码呢? 在 ILSpy 中有一个 used by 功能,在这里可以用起来了。

双击 used by 就能看到真正的调用代码,简化后如下:


public async Task DistributionBundle(List<Bundle> list, List<xxx> bwdList, xxx item, List<xxx> sumDetails, List<xxx> details, BundleParameter bundleParameter, IEnumerable<dynamic> labels)
{
	int num = 0;
	foreach (xxx detail in sumDetails)
	{
		IEnumerable<xxx> woDetails = details.Where((xxx w) => w.Size == detail.Size && w.Color == detail.Color);
		foreach (xxx item2 in woDetails)
		{
            xxx
		}
		woDetails = woDetails.OrderBy((xxx s) => s.Seq).ToList();
		num++;
        xxx
		Bundle bundle = new Bundle();
		Bundle bundle2 = bundle;
		bundle2.BundleId = await _repo.CreateBundleId();
		
		foreach (xxx item3 in woDetails)
		{
			item3.TaskQty = item3.WoQty + Math.Ceiling(item3.WoQty * item3.OverCutRate);
			decimal value = default(decimal);
		}

		await DistributionBundle(list, bwdList, item, sumDetails, details, bundleParameter, labels);
	}
}

仔细看上面这段代码, 我去, await DistributionBundle(list, bwdList, item, sumDetails, details, bundleParameter, labels); 又调用了自身,看样子是某种条件下陷入了一个死递归。。。

有些朋友可能要问,除了经验之外,能从 dump 中分析出来吗?当然可以,从 500w+ 中抽一个看看它的 !gcroot 即可。


0:000> !DumpHeap /d -mt 00007ffdeb210098
         Address               MT     Size
000001a297913a68 00007ffdeb210098      264     
000001a297913b70 00007ffdeb210098      264  

0:000> !gcroot 000001a297913a68
Thread 5ac:
    000000470B1EE4E0 00007FFE45103552 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2922]
        rbp+10: 000000470b1ee550
            ->  000001A297A25D88 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions+<RunAsync>d__4, Microsoft.Extensions.Hosting.Abstractions]]
            ->  000001A29796D8C0 Microsoft.Extensions.Hosting.Internal.Host
            ...
            ->  000001A298213248 System.Data.SqlClient.TdsParserStateObjectNative
            ->  000001A32E6AB700 System.Threading.Tasks.TaskFactory`1+<>c__DisplayClass38_0`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient],[System.Data.CommandBehavior, System.Data.Common]]
            ->  000001A32E6AB728 System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]]
            ->  000001A32E6ABB18 System.Threading.Tasks.StandardTaskContinuation
            ->  000001A32E6ABA80 System.Threading.Tasks.ContinuationTaskFromResultTask`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]]
            ->  000001A32E6AB6C0 System.Action`1[[System.Threading.Tasks.Task`1[[System.Data.SqlClient.SqlDataReader, System.Data.SqlClient]], System.Private.CoreLib]]
            ->  000001A32E6AB428 System.Data.SqlClient.SqlCommand+<>c__DisplayClass130_0
            ...
            ->  000001A32E6ABC08 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.String, System.Private.CoreLib],[Dapper.SqlMapper+<QueryRowAsync>d__34`1[[System.String, System.Private.CoreLib]], Dapper]]
            ->  000001A32E6ABD20 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.String, System.Private.CoreLib],[xxx.DALs.xxx.BundleRepo+<CreateBundleId>d__12, xxx]]
            ->  000001A32E6ABD98 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A32E6A6BD8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A433250520 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A32E69E0F8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A433247D28 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A433246330 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A32E69A568 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ->  000001A433245408 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[xxx.BundleBiz+<DistributionBundle>d__20, xxx]]
            ...

从调用栈来看,代码貌似是从数据库读取记录的过程中陷入死循环的。

4. 为什么没有出现栈溢出

一看到无限循环,我相信很多朋友肯定要问,为啥没出现堆栈溢出,毕竟默认的线程栈空间仅仅 1M 而已,从 !gcroot 上看,这些引用都是挂在 5ac 线程上,也就是下面输出的 主线程 ,而且主线程栈也非常干净。


0:000> !t
ThreadCount:      30
UnstartedThread:  0
BackgroundThread: 24
PendingThread:    0
DeadThread:       5
Hosted Runtime:   no
                                                                                                            Lock  
 DBG   ID     OSID ThreadOBJ           State GC Mode     GC Alloc Context                  Domain           Count Apt Exception
   0    1      5ac 000001A29752CDF0  202a020 Preemptive  0000000000000000:0000000000000000 000001a29754c570 0     MTA 
   4    2     1e64 000001A29752A490    2b220 Preemptive  0000000000000000:0000000000000000 000001a29754c570 0     MTA (Finalizer) 
...


0:000> !clrstack 
OS Thread Id: 0x5ac (0)
        Child SP               IP Call Site
000000470B1EE1D0 00007ffe5eb30544 [GCFrame: 000000470b1ee1d0] 
000000470B1EE318 00007ffe5eb30544 [HelperMethodFrame_1OBJ: 000000470b1ee318] System.Threading.Monitor.ObjWait(Boolean, Int32, System.Object)
000000470B1EE440 00007ffe45103c25 System.Threading.ManualResetEventSlim.Wait(Int32, System.Threading.CancellationToken)
000000470B1EE4E0 00007ffe45103552 System.Threading.Tasks.Task.SpinThenBlockingWait(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2922]
000000470B1EE550 00007ffe451032cf System.Threading.Tasks.Task.InternalWaitCore(Int32, System.Threading.CancellationToken) [/_/src/System.Private.CoreLib/shared/System/Threading/Tasks/Task.cs @ 2861]
000000470B1EE5D0 00007ffe45121b04 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(System.Threading.Tasks.Task) [/_/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/TaskAwaiter.cs @ 143]
000000470B1EE600 00007ffe4510482d System.Runtime.CompilerServices.TaskAwaiter.GetResult() [/_/src/System.Private.CoreLib/shared/System/Runtime/CompilerServices/TaskAwaiter.cs @ 106]
000000470B1EE630 00007ffe4de36595 Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(Microsoft.Extensions.Hosting.IHost) [/_/src/Hosting/Abstractions/src/HostingAbstractionsHostExtensions.cs @ 49]
000000470B1EE660 00007ffde80f3b4b xxx.Program.Main(System.String[])
000000470B1EE8B8 00007ffe47c06c93 [GCFrame: 000000470b1ee8b8] 
000000470B1EEE50 00007ffe47c06c93 [GCFrame: 000000470b1eee50] 

如果你稍微了解一点异步的玩法,你应该知道这其中有一个 IO完成端口 的概念,它可以实现 句柄ThreadPool 的绑定,无限递归只不过是进了 IO完成端口 的待回调队列中而已,理论上和栈空间没什么关系,也就不会出现栈溢出了。

三:总结

本次内存泄漏的事故主要还是因为程序员的大意,也许是长期的 996 给弄恍惚了

以上是关于记一次 .NET 某风控管理系统 内存泄漏分析的主要内容,如果未能解决你的问题,请参考以下文章

记一次 .NET 某智能服装智造系统 内存泄漏分析

记一次 .NET 某电厂Web系统 内存泄漏分析

记一次 .NET 某HIS系统后端服务 内存泄漏分析

记一次 .NET 某HIS系统后端服务 内存泄漏分析

记一次 .NET 某消防物联网 后台服务 内存泄漏分析

记一次 .NET 某外贸Web站 内存泄漏分析