适用于: .NET Core 2.1、.NET Core 3.1、.NET 5
本文可帮助你分析性能问题,并介绍如何使用 createdump 和 ProcDump 在 Linux 中手动捕获 .NET Core 内存转储文件。
先决条件
遵循以下故障排除实验室的最低要求如下:
- ASP.NET 核心应用程序,用于演示低 CPU 和高 CPU 性能问题和崩溃问题。
- 打开核心转储时,lldb 调试器已安装并配置为加载 SOS 扩展。
如果已遵循本系列的上一部分,则应准备好进行以下设置:
- Nginx 配置为托管两个网站:
- 第一个使用 myfirstwebsite 主机标头 (
http://myfirstwebsite
) 侦听请求,并将请求路由到在端口 5000 上侦听的演示 ASP.NET Core 应用程序。 - 第二个使用 buggyamb 主机标头 (
http://buggyamb
) 侦听请求,并将请求路由到侦听端口 5001 的第二个 ASP.NET 核心示例 buggy 应用程序。
- 第一个使用 myfirstwebsite 主机标头 (
- ASP.NET Core 应用程序应作为服务运行,这些服务会在服务器重启或应用程序停止响应时自动重启。
- Linux 本地防火墙已启用并配置为允许 SSH 和 HTTP 流量。
注意
如果设置尚未准备就绪,请转到“第 2 部分创建并运行 ASP.NET Core 应用”。
本实验室的目标
到目前为止,在此故障排除系列中,你已经分析了崩溃问题。 在此实验室中,你将有机会分析性能问题,并了解如何使用 createdump 和 ProcDump 手动捕获内存转储文件。
重现问题
在上一部分中,通过选择 “慢 速”链接测试了第一个“慢速”方案。 执行此操作时,页面会正确加载,但速度比预期慢得多。 在本部分中,你将使用 BuggyAmb 负载生成器 功能来排查此性能问题。 这是一项“实验性”功能,最多向任何有问题的资源发送六个同时请求。 限制为 6,因为它使用 jQuery 和 Ajax 调用发出请求。 Web 浏览器将大多数 Ajax 请求限制为给定 URL 的六个并发请求。 若要了解如何使用负载生成器重现不同的方案,请参阅实验性“负载生成器”。
若要重现该问题,请打开问题页,选择“加载生成器”,然后在“慢速”方案中发送六个请求。
以下列表显示了最终应在浏览器中看到的内容。 显示的响应时间很高。 预期的响应时间小于一秒。 这是从应用程序登陆页中选择 “预期结果 ”链接时可以看到的。
这是你将进行故障排除的问题。
监视症状
请记住,良好的故障排除会话首先定义问题并了解症状。 当你尝试通过生成负载来重现问题时,你将用于 htop
监视托管 ASP.NET Core 应用程序的进程的进程内存和 CPU 使用率。 如果你不记得什么是 htop
,请检查以前的系列部件。
在尝试重现问题之前,请先设置应用程序应执行方式的基线。 选择“预期结果”,或使用“负载生成器”功能将多个请求发送到预期结果方案。 然后,检查问题未显示时 CPU 和内存使用情况的外观。 你将用于 htop
检查 CPU 和内存使用情况。
运行 htop
,并对其进行筛选以仅显示属于运行 buggy 应用程序的用户的进程。 在本例中,目标 ASP.NET Core 应用程序的用户是 www-data。 按 U 键从列表中选择该 www-data 用户。 同时按 Shift+H 隐藏线程。 可以看到,www-data 上下文中运行了四个进程,其中两个进程是 Nginx 进程。 其他应用程序用于 buggy 应用程序和设置环境时创建的演示应用程序。
由于尚未重现性能问题,因此请注意,所有 CPU 和内存使用率统计信息目前都很低。
现在,返回到客户端浏览器,并使用负载生成器向慢速方案发送六个请求。 之后,请快速返回到 Linux 设备,并观察进程资源消耗情况 htop
。 应看到 buggy 应用程序的 CPU 使用率急剧增加,内存使用率会上下波动。
注意
由于此输出取自配备两个逻辑 CPU 的虚拟机, htop
因此 CPU 使用率超过 100%。
最终处理所有请求后,CPU 和内存使用率会减少。 CPU 和内存使用趋势都应让你怀疑处理请求期间应用程序中可能存在大量 GC(垃圾回收器)使用情况。
收集核心转储文件
排查性能问题时,会捕获和分析连续内存转储文件。 捕获多个转储文件背后的想法很简单:进程转储是进程内存的快照。 它不包含过去的信息。 若要排查性能问题,应捕获多个手动内存转储文件或核心转储文件,以便比较线程和堆等。
使用以下建议选项按需捕获手动内存转储文件:
- Createdump
- Procdump
- Dotnet-dump
Createdump
Createdump 与 .NET Core 运行时一起包含在一起。 它位于运行时目录中。 可以使用命令查找运行时目录路径 dotnet --list-runtimes
。
由于 buggy 应用程序是 .NET Core 3.1 应用程序,因此 createump 的完整路径为 /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump。
此命令的最简单形式是 createdump <PID>
。 这将为目标进程写入一个核心转储。 可以通过添加 -f
开关来指示工具在何处创建转储文件: createdump <PID> -f <filepath>
。 在本练习中 ~/dumps/ ,请在目录中创建转储文件。
你将捕获 BuggyAmb 进程的两个连续内存转储文件 10 秒。 在重现“响应缓慢的请求”问题时,必须捕获转储文件。 若要开始,首先必须找到进程的 PID。 htop
使用或systemctl status buggyamb.service
命令。 在以下列表中,进程 PID 为 11724。
若要创建转储文件,请执行以下步骤:
- 创建第一个文件:
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.1.%d
. - 在写入第一个转储文件后等待 10 秒。
- 创建第二个文件:
sudo /usr/share/dotnet/shared/Microsoft.NETCore.App/3.1.10/createdump 11724 -f ~/dumps/coredump.manual.2.%d
最后,应该有两个内存转储文件。 请注意每个转储文件的大小。
分析 lldb 中的转储文件
你应该已经知道如何在 lldb 中打开转储文件。 在两个不同的 SSH 会话中打开 lldb 中的两个文件。
你的目标是制定一个关于可能导致性能问题的理论。 在出现问题时,你已知道 CPU 和内存使用率很高。 若要检查托管内存,可以使用 dumpheap -stat
该命令。 在开始之前,请快速查看第一个转储文件。
clrthreads
运行命令以获取托管线程的列表。
注意
其中一个线程将 GC 模式设置为 “协作”,另一个线程设置为 “抢占”。
如果线程的 GC 模式设置为 “抢占”,这意味着 GC 可以随时挂起此线程。 相比之下,协作模式意味着 GC 必须等待线程在暂停之前切换到抢占模式。 当线程运行托管代码时,它处于协作模式。
首先,在协作模式下检查线程。 协作线程的调试器的线程 ID 在示例列表中为 19 。 重复练习时,ID 将有所不同。 通过运行 thread select 19
切换到线程,然后运行 clrstack
以列出托管调用堆栈。 buggy 应用程序的“慢”页正在执行字符串 concat 操作。
这应该会使你变得可疑,因为你应该知道字符串 concat 操作成本高昂。 这是因为 .NET 中的字符串对象是不可变的,这意味着分配后无法更改其值。 请考虑以下伪代码片段:
string myText = "Debugging";
myText = myText + " .NET Core";
myText = myText + " is awesome";
此代码在内存中创建多个字符串: Debugging
、 Debugging .NET Core
和 Debugging .NET Core is awesome
。 必须创建三个不同的字符串对象来生成一个最终字符串(连接)。 如果这种情况经常发生,可能会产生内存压力,以便触发 GC。
这个理论听起来很有希望。 但是,应尝试验证它是否正确。 在查看托管堆之前,在已定位在线程上下文中时,请检查从此线程引用的对象,以尝试确定字符串和 string[]
对象值是什么。 运行 dso
并专注于字符串和字符串数组。
尝试检查字符串数组。 使用对象的地址运行 dumpobj
。 但是,请注意,这仅演示有问题的对象是数组。 SOS 提供用于 dumparray
调查数组的命令。 运行 dumparray 00007faf309528c8
以获取数组中项的列表。 (请记住,数组对象的地址在要检查的转储文件中将有所不同。
dumpobj
使用数组中包含的结果字符串地址再次运行该命令。 选择一些地址,并调查这些地址。
这些字符串类似于页面显示的产品表中的字符串。
请注意,如果字符串较大,lldb(或 SOS)可能不会显示字符串值。 在这种情况下,其中一个选项是使用 lldb 的本机命令来检查本机内存地址。 这类似于在 WinDbg 中使用 d*
命令(例如 dc
)。
以下命令读取给定内存位置的本机内存,并显示前 384 个字节。 列表使用其中一个字符串地址来演示它。 正在运行的命令是 memory read -c 384 00007fb14d5da040
。
线程堆栈引用的字符串数似乎证实了字符串 concat 问题导致性能问题的理论。
但是,调查尚未完成。 你有两个内存转储文件。 因此,你将比较托管内存堆,并检查堆的时间变化情况。
在每个 dumpheap -stat
转储文件中运行命令。 下面是第一个文件中的内容。 在下面的列表中,有 105,401 个字符串对象,字符串对象的总大小约为 480 MB。 另请注意,内存可能已分段,碎片的原因似乎与字符串数组对象和 System.Data.DataRow
对象相关。
继续运行第二个转储文件中的相同 dumpheap -stat
命令。 应会看到碎片统计信息的更改,但这在调查上下文中并不重要。 重要部分是字符串对象的数量,以及这些对象的大小显著增加。
同时,对象数 System.Data.DataRow
也会增加。
你可能怀疑存在涉及大型对象堆(LOH)的问题。 因此,你可能想要检查 LOH 对象。 在这种情况下,应运行 dumpheap -stat -min 85000
命令。 以下列表包含第一个内存转储的 LOH 统计信息。
下面是第二个内存转储的 LOH 统计信息。
这也清楚地显示了堆的增加。 这一切似乎都与 string
对象相关。
最后,如果要从 LOH 中选择一个“live”对象来查找其根,该怎么办? 在本例中,“实时”表示对象植根于某个位置,因此,应用程序正在积极使用,以便 GC 进程不会删除它。
处理这种情况很容易。 运行 dumpheap -stat -min 85000 -live
。 此命令仅显示位于某个位置的对象。 在此示例中,只有位于 LOH 中的对象的正确实例 string
。
使用对象的 MT 地址 string
获取这些实时对象的地址列表。 运行 dumpheap -mt 00007fb1602c0f90 -min 85000 -live
。
现在,从生成的列表中随机选取一个地址。 在以下屏幕截图中,将显示列表中的第三个地址。 可以通过运行 dumpobj
来尝试检查所选地址。 但是,由于这是一个大对象,调试器不会显示值。 因此,相反,请再一次检查本机内存地址,你将看到这是一个 string
对象,类似于在响应缓慢的页面上的产品表列表中找到的内容。
检查所列出的对象的根目录。 为此,请使用 SOS gcroot
命令。 此命令只是以最简单的形式将对象的地址作为参数。 如你所看到的,这 string
根植根于运行“慢速”页面的线程。 你甚至可以看到源文件名称和行号信息。
注意
查看源文件名称和行号信息取决于故障排除的位置以及符号是否已正确设置。 在最坏的情况下,至少可以恢复线程 ID。 在以下列表中, b6c 是托管线程 ID。 如果运行 clrthreads
,可以找到相应的线程 ID。
如上面的屏幕截图所示,托管线程 ID b6c 的调试器线程 ID 为 23。 切换到线程 23,并检查托管调用堆栈。 如前所述,此线程还应执行字符串 concat 操作。
如果使用命令检查本机调用堆栈 bt
,你可能会注意到 GC 正在为此线程分配内存。
此证据证实了该问题与大量字符串串联操作相关的理论,这些操作在处理“慢”页时会触发的更大字符串。
此类问题的解决方案不在本系列的范围内。 但是,请注意,使用类实例而不是字符串 concat 操作可以轻松实现 StringBuilder
解决方案。