记一次 .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.Bundle
和 AsyncTaskMethodBuilder
两大类对象给吃掉了,数量都高达 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 某上市工业智造 CPU+内存+挂死 三高分析
一:背景 1. 讲故事 上个月有位朋友加wx告知他的程序有挂死现象,询问如何进一步分析,截图如下: 看这位朋友还是有一定的分析基础,可能玩的少,缺乏一定的分析经验,当我简单分析之后,我发现这个dump ...
- 记一次 .NET 某电厂Web系统 内存泄漏分析
一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...
- 记一次 .NET 某智慧水厂API 非托管内存泄漏分析
一:背景 1. 讲故事 七月底的时候有位朋友在wx上找到我,说他的程序内存占用8G,托管才占用1.5G,询问剩下的内存哪里去了?截图如下: 从求助内容看,这位朋友真的太客气了,动不动就谈钱,真伤感情, ...
- 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析
一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...
- 记一次 .NET 医院CIS系统 内存溢出分析
一:背景 1. 讲故事 前几天有位朋友加wx求助说他的程序最近总是出现内存溢出,很崩溃,如下图: 和这位朋友聊下来,发现他也是搞医疗的,哈哈,.NET 在医疗方面还是很有市场的,不过对于内存方面出的问 ...
- 记一次 .NET 某HIS系统后端服务 内存泄漏分析
一:背景 1. 讲故事 前天那位 his 老哥又来找我了,上次因为CPU爆高的问题我给解决了,看样子对我挺信任的,这次另一个程序又遇到内存泄漏,希望我帮忙诊断下. 其实这位老哥技术还是很不错的,他既然 ...
- 记一次 .NET 某外贸Web站 内存泄漏分析
一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...
- 记一次 .NET 某三甲医院HIS系统 内存暴涨分析
一:背景 1. 讲故事 前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析? 和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源, ...
- 记一次 .NET 某WMS仓储打单系统 内存暴涨分析
一:背景 1. 讲故事 七月中旬有一位朋友加wx求助,他的程序在生产上跑着跑着内存就飙起来了,貌似没有回头的趋势,询问如何解决,截图如下: 和这位朋友聊下来,感觉像是自己在小县城当了个小老板,规律的生 ...
随机推荐
- Golang通脉之包的管理
在工程化的开发项目中,Go语言的源码复用是建立在包(package)基础之上的. 包(package)是多个Go源码的集合,是一种高级的代码复用方案,Go语言提供了很多内置包,如fmt.os.io等. ...
- 解决GitHub访问慢
话不多说,上干货~~~ 1. 打开 http://tool.chinaz.com/dns/ ,在输入框中填写 github.com,然后点击检测按钮,会列出响应ip,如图: 2. 找到hosts文件, ...
- 【c++ Prime 学习笔记】目录索引
第1章 开始 第Ⅰ部分 C++基础 第2章 变量和基本类型 第3章 字符串.向量和数组 第4章 表达式 第5章 语句 第6章 函数 第7章 类 第 Ⅱ 部分 C++标准库 第8章 IO库 第9章 顺序 ...
- Java:异常小记
Java:异常小记 对 Java 中的 异常 ,做一个微不足道的小小小小记 Error 和 Exception 相同点: Exception 和Error 都是继承了 Throwable 类,在 Ja ...
- [技术博客]使用pylint实现django项目的代码风格检查
使用pylint实现django项目的代码风格检查 前言 一个项目大多都是由一个团队来完成,如果没有统一的代码规范,那么每个人的代码的风格必定会有很大的差别.且不说会存在多个人同时开发同一模块的情 ...
- Go语言核心36讲(Go语言进阶技术八)--学习笔记
14 | 接口类型的合理运用 前导内容:正确使用接口的基础知识 在 Go 语言的语境中,当我们在谈论"接口"的时候,一定指的是接口类型.因为接口类型与其他数据类型不同,它是没法被实 ...
- 学习手册 | MySQL篇 · 其一
InnoDB关键特性 插入缓冲(Insert Buffer) 问题: 在InnoDB插入的时候,由于记录通常都是按照插入顺序,也就是主键的顺序进行插入的,因此,插入聚集索引是顺序的,不需要随机IO ...
- Manacher(马拉车)
Able was I ere I saw Elba. ----Napoléon Bonaparte(拿破仑) 一.回文串&回文子串 这个很好理解. 如果一个字符串正着读和反着读是一 ...
- 转载: VIVADO的增量综合流程
http://xilinx.eetrend.com/content/2019/100044286.html 从 Vivado 2019.1 版本开始,Vivado 综合引擎就已经可以支持增量流程了.这 ...
- 【linux命令】 磁盘管理
du du是查看硬盘的使用情况,统计文件或目录的空间大小. -a 显示所有目录或文件的大小 -b 以byte为单位,显示目录或文件的大小 -c 显示目录或文件的总和 -k 以KB为单位输出 -m 以M ...