Dump
dump指转储,一般用来创建进程快照。它可以在不停止应用的情况下,直接将模块列表、线程列表、堆栈信息、异常信息、句柄信息等所有内存信息保存下来,帮助开发者分析生产环境问题等。
这篇博客主要介绍dotnet-dump的使用以及如何在Visual Studio中进行dump分析。
dotnet-dump
dotnet-dump是用于收集转储的跨平台命令行工具, Visual Studio和windbg也具有转储收集功能。
使用命令行安装dotnet-dump。
dotnet tool install --global dotnet-dump
dotnet-dump ps
dotnet-dump 只能访问托管代码, 使用dotnet-dump ps命令可以查看可以收集转储的 dotnet 进程。
dotnet-dump collect
使用dotnet-dump collect收集转储信息并保存到本地。
dotnet-dump collect -p 16036 -o C:\Users\mahua\Desktop\dump\test.dmp --type full
其中type按照收集数据范围由小到大分为:mini,triage, heap,full。
triage和heap转储数据范围一致,但triage去除了个人用户信息,如路径密码等。
如果在Windows环境转储时提示访问输出路径被拒绝,则需要提升dotnet-tools的运行权限,默认是到C:User\%User%\.dotnet\路径下,提升用户对tools文件夹的访问权限。
dotnet-dump analyze
dotnet-dump analyze命令非常繁琐,建议放弃,我们可以直接把dump文件拖到Visual Studio里进行分析。
Dump分析
为了方便模拟各种应用场景,我们可以使用dotnet官方提供的代码示例:DiagnosticScenarios · dotnet/samples。代码中包含了几个简单api模拟了死锁,内存飙升,内存泄漏,CPU飙升等场景。
我们将程序运行起来,通过swagger页面调用相应的接口来测试dump分析。
死锁分析
★运行诊断分析★
通过swagger页面调用死锁接口,接口会因为死锁而不返回结果,抓取dump文件,拖入Visual Studio进行分析。
点击运行诊断分析,可以看到Visual Studio直接给出了分析结果:检测到已死锁线程。
★使用调试窗口★
此时Visual Studio处于调试状态,而且dump文件包含了当时的堆栈、线程等信息,所以我们可以使用调试模式下的各种监视窗口来分析问题,详细信息可以看我上一篇博客:Visual Studio 高级调试-代码调试,这里我们主要查看并行堆栈窗口。
可以看到从左边起,前3个线程254,11,35都在等待锁的持有者25180,而线程25180则在等待另一个锁的持有者线程6248,线程6248也在等待锁的持有者25180。
那么问题就找出来了:线程25180和线程6248各持有一个锁,并且都在等待对方将锁释放出来,因此产生了死锁。
将鼠标放置在堆栈帧上查看提示,双击图示堆栈帧将来到线程6248发生死锁的位置,可以看到这里线程6248已经获得了锁o1,正在请求锁: o2。
此时锁o2应该是被线程25180所持有,而它正在请求锁o1,我们点击倒数第2个堆栈帧,可以来到线程25180死锁的地方,从而证明这一点。
堆栈信息遵守"后来者居上"原则,最近发生的会放在最上面。
★死锁的堆栈分析原理★
虽然Visual Studio非常智能的给出了结论:存在死锁。但我们仍有必要知道是如何通过堆栈分析得出这一结论的。
首先发现大多数线程共享一个公共调用堆栈
该调用堆栈调用了某方法,而该方法又调用了 Monitor.ReliableEnter。这表示线程正等待获取某个锁。
查看同步块表(不懂同步块的同学看这里C#引用类型实现原理),确定该锁已经被其他线程所持有。
进而分析得出两个线程分别持有一个锁,并且都在等待获取对方的锁,因此判定存在死锁。
内存飙升分析
调用接口模拟20秒内,内存使用升高的场景。同样的,我们使用dump转储并拖入Visual Studio进行分析。
诊断分析
对于内存飙升这种情况,诊断分析不会直接给出结论,但我们可以注意到其中大对象的创建,点击发现一个存放Customer的数组使用了1670万字节的空间。我们需要使用内存分析,进一步查找详细信息。
调试托管内存
我们可以选择调试托管内存,得到托管堆视图。看到这里基本可以断定,应用在该时刻创建了大量的Customer对象,并且没有及时释放,导致了内存占用升高。
查看并行堆栈
那么我们能否定位到代码位置呢?此时可以查看并行堆栈,它会显示dump时正在运行的线程信息,我们可以查看是哪些线程正在执行创建Customer的程序,并且根据pdb定位到代码位置。
再次dump对比
我们可以再次dump,并且进行内存分析,可以看到Customer对象以及大部分被回收了。
内存泄漏分析
调用接口,模拟20万kb内存泄漏发生,内存泄漏的显著特点是内存占用持续升高,并且没有减小的趋势。同样得到dump文件之后拖入Visual Studio进行分析。
按照理论,我们应该先分析内存整体状态,找出占用内存较多的对象,这里大多数对象是 String 或 Customer 对象。然后再通过对System.String 实例使用 gcroot 命令,以查看对象的根方式和原因。
但是,Visual Studio已经帮我们做好了一切,甚至已经把嫌疑最大的对象标记出来了。
查看对象引用关系
这里的内存泄漏是由静态根引起的,不了解静态根和GC原理的同学可以看这里:CLR内存管理机制
我们可以看到testwebapi.Controllers.Customer对象被创建了100万个,并且没有被回收,双击查看对象实例,发现它们最终都间接的被testwebapi.Controllers.Processor这个静态变量所引用,因此无法被释放。
总结
好了,关于dump分析的内容就写到这里了,dump主要用在无法直接进行调试的生产环境或CI环境。dump分析命令非常复杂且低效,借助Visual Studio我们可以更加高效的分析dump所记录的数据。
下篇我们将介绍Visual Studio调试时功能强大的程序诊断工具以及企业版的特有的IntelliTrace等内容。