用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
· 小村 豪 · PerfView, dotnet-trace, ETW, 性能排查, .NET, CSharp, 故障排查, Windows开发, 技术咨询
上一篇《Windows 事件日志・ETW 入门》介绍了 ETW 是什么,以及如何用 EventSource 定义自定义事件,并把用 PerfView 分析的部分列为“不在本文范围内”。这篇文章正是那篇的续篇。主题是当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,如何用 PerfView 与 dotnet-trace 定位到具体是哪个函数出了问题。
需要说明的是,本文以 CPU 和响应时间的排查为主题。内存持续增长的问题排查方法,见《在 .NET 中区分 GC 等待与内存泄漏》;崩溃类问题则整理在《用 WinDbg + SOS 读取崩溃转储》中。
1. 先说结论
- “变慢”有两种情况。CPU 被用满而变慢(CPU bound),以及 CPU 明明很闲却被拖慢(阻塞)。 前者需要 CPU 采样,后者需要 ThreadTime(上下文切换)采集,仅看默认的 CPU 采集是看不出后者原因的。1
- 不确定该用哪个工具时,先从 dotnet-trace 开始。 它是基于 EventPipe 的跨平台工具,只要是启动目标进程的同一个用户,就能不需要管理员权限进行采集。2 采集到的
.nettrace可以用 PerfView 或 Visual Studio 打开。3 - 需要用到 PerfView 的场景,是想查看整台机器、原生代码,甚至阻塞时间的时候。 由于是基于 ETW 的,因此也能处理内核事件和原生代码的调用栈。相应地,启动 ETW 会话需要管理员权限。3
- CPU 采样默认每 1 毫秒(按处理器)采集一次。 把 1 个样本解读为约 1ms 的 CPU 时间,作为判断基准,建议总样本数达到 1,000 个以上(最好接近 5,000 个)再下结论。样本数过少时,排名靠前的函数可能只是偶然。1
- 解读数字的关键,始于 inclusive(自身+被调用的一切)与 exclusive(自身)的区分。 exclusive 较大的函数是“实际在消耗 CPU 的地方”,inclusive 较大的函数则说明“其下某处开销很大”。
- 测量之前,先把“哪里慢”固定到一个具体操作上。 用“整体感觉很慢”这种状态去采集,是无法解读出结果的。要先把操作和耗时固定下来,比如“这份报表输出要花 40 秒”,然后才开始采集。前后对比测量的思路,也可以参考《在 Windows 上正确比较不同版本程序速度的方法》。
按症状整理该从何处入手的判断表如下。
| 症状 | 首选工具 | 要看的内容 |
|---|---|---|
| CPU 一直居高不下 | dotnet-trace collect / PerfView 的 CPU Stacks | exclusive 较大的函数 |
| CPU 很低但处理很慢、会卡住 | PerfView(ThreadTime 采集) | 哪个线程在等什么而被阻塞 |
| 变慢了,但先想了解大致趋势 | dotnet-counters | CPU 使用率、GC 频率、ThreadPool 队列积压 |
| 怀疑 GC 过多 | dotnet-counters → dotnet-trace(GC 事件) | GC 次数・停顿时间、分配较多的位置 |
| 想知道特定业务处理中哪一步慢 | EventSource 自定义事件+以上方法 | 自定义事件之间的耗时,以及该区间的调用栈 |
2. 理清工具之间的关系 ── ETW 与 EventPipe
登场的工具看起来很多,但底层基础其实只有两种。
- ETW(Event Tracing for Windows):贯穿整个操作系统的跟踪基础设施。可以用同一条时间轴从内核一直记录到应用层,但会话的启动与停止需要管理员权限,且仅限 Windows。3
- EventPipe:内置于 .NET 运行时中的跟踪机制。不需要管理员权限,在各操作系统上行为一致,但代价是可见范围仅限于托管代码与运行时,无法获取内核事件或原生调用栈。2
| dotnet-trace(EventPipe) | PerfView(ETW) | |
|---|---|---|
| 管理员权限 | 不需要(针对同一用户的进程) | 需要 |
| 目标范围 | 指定的单个 .NET 进程 | 整台机器(所有进程+内核) |
| 原生调用栈 | 无法获取 | 可以获取 |
| 阻塞时间(上下文切换) | 无法获取 | 可通过 ThreadTime 采集获取 |
| 支持的操作系统 | Windows / Linux / macOS | 仅 Windows |
只要把这张对照表记在心里,“先用 dotnet-trace 轻量采集,不够的话再用 PerfView 连 ETW 一起采集”的取舍思路就会自然而然地成形。
3. 采集之前 ── 先用 dotnet-counters 观察 10 分钟的趋势
比起直接采集跟踪数据,先用 dotnet-counters 浏览运行时的关键指标,往往更快捷。4
dotnet tool install --global dotnet-counters
dotnet-counters ps
dotnet-counters monitor --process-id <PID> --counters System.Runtime
这里要看的是 CPU 使用率、GC 堆大小与 GC 次数、ThreadPool 的队列长度与线程数、异常次数。如果这个阶段就能看出“GC 每秒运行多次”“ThreadPool 队列持续增长”这类趋势,就能缩小接下来该采集哪种跟踪数据的范围(是分配问题,还是阻塞问题)。关于内存相关趋势的解读方法,详见《区分 GC 等待与内存泄漏的方法》一文。
4. 用 dotnet-trace 采集跟踪数据
dotnet-trace 是基于 EventPipe 的采集工具。5 安装与基本采集方式如下。
dotnet tool install --global dotnet-trace
dotnet-trace ps
dotnet-trace collect --process-id <PID> --duration 00:00:00:30
如果不指定任何选项,就会按包含运行时关键事件与线程采样的默认配置文件进行采集。过去存在的 cpu-sampling 这个配置文件已经废弃,现在的默认组合是 dotnet-common 与 dotnet-sampled-thread-time。5 根据用途,也可以选择 --profile gc-verbose(GC 与对象分配采样)或 --profile gc-collect(仅以低开销记录 GC 的发生)。
如果要连同上一篇文章中创建的 EventSource 自定义提供程序一起采集,加上 --providers 即可。
dotnet-trace collect --process-id <PID> --providers KomuraSoft-OrderService
采集结果是一个 .nettrace 文件。打开方式有三种:用 Visual Studio 或 PerfView 打开,或者转换成 speedscope 格式在浏览器中查看。5
dotnet-trace convert trace.nettrace --format Speedscope
speedscope 格式是可以在 speedscope.app 打开的轻量级火焰图,适合直观地查看“时间都花在哪个函数下面”。若要精确到函数级别的细致分析,PerfView 的调用栈查看器更强大,接下来进入下一章。
5. 用 PerfView 排查 CPU ── 调用栈查看器的解读方法
PerfView 是微软免费公开的性能排查工具,只需从 GitHub 发布页面下载一个 PerfView.exe 文件即可运行(无需安装)。6
5.1 采集数据
在 GUI 中,基本流程是通过“Collect > Collect”打开对话框,Start Collection → 复现目标操作 → Stop Collection。使用命令行的话形式如下(因为要启动 ETW 会话,需以管理员身份运行 3)。
PerfView collect /nogui /acceptEULA /maxCollectSec:30
默认采集包含所有处理器上每 1 毫秒一次的 CPU 样本(含调用栈),以及 CLR 的关键事件。1 采集期间的开销,在默认配置下大致是百分之几的量级。7
5.2 解读 CPU Stacks
打开采集结果(.etl.zip),在“CPU Stacks”中选择目标进程,就会打开调用栈查看器。首先要看的是 By Name 标签页中的两列。
- Exc(exclusive):该函数自身正在执行时的样本数。是实际消耗 CPU 的位置。
- Inc(inclusive):该函数及其调用的一切的样本总数。表明其下某处开销较大。
基本的解读方式是从 Exc 排名最高的开始,判断“这是自己的代码,还是运行时・库”。如果自己的代码出现在 Exc 排名靠前的位置,那么该函数的算法或循环就是直接的排查目标。如果是 System.String 相关内容或 JSON 序列化器排在 Exc 前列,就在 Callers 标签页中用 Inc 往回追溯,找出自己的代码中究竟是从哪里大量调用它的。
另一个需要检查的重点是样本数本身。CPU 样本是以 1ms 间隔统计的数据,样本数过少就容易受偶然因素影响。参考标准是总数达到 1,000 个以上,最好接近 5,000 个再下结论,如果不够就重复执行目标操作以延长采集时间。1
另外,如果自有模块的函数名显示为地址,说明符号(PDB)没有被正确解析。提前准备好构建产物对应的 PDB,这一点本身就决定了能否进行排查。这个话题在《什么是 PDB(程序数据库)》中有详细说明。
6. “CPU 明明很闲却很慢” ── 用 ThreadTime 查看阻塞时间
实务中一半以上的“变慢”,原因并不在 CPU,而是等待。等锁、等数据库或 HTTP 响应、文件 I/O、因 Task.Result 造成的近似死锁的等待。这类问题在 CPU 样本中只会体现为“压根没有使用 CPU 的时间”,因此默认采集是看不出原因的。
在 PerfView 中,采集时启用 Thread Time 选项,就会额外记录上下文切换事件,让每个线程“使用 CPU 的时间”与“被阻塞的时间”能够一起被追踪。1
PerfView collect /nogui /acceptEULA /threadTime /maxCollectSec:30
分析时打开“Thread Time Stacks”视图。它和 CPU Stacks 是同一个调用栈查看器,区别在于样本中除了 CPU 时间,还包含 BLOCKED_TIME(被阻塞的时间)。把范围收窄到慢速操作的时间段后,观察负责处理的线程的 BLOCKED_TIME 集中在哪个调用栈(哪种等待)上,就能得出类似“总共 40 秒里有 35 秒是在等这个 HTTP 调用”这样的明细。
UI 卡死的症状(操作后数秒无响应)的本质,几乎都是 UI 线程被阻塞。用 ThreadTime 定位到 UI 线程被阻塞在哪里之后,长期解决方案就是把同步等待改造为异步设计。这方面的设计思路,请参考《用一张表梳理 WPF/WinForms 的 async 与 UI 线程》。
需要注意的是,ThreadTime 采集会针对每一次上下文切换记录事件,因此数据量会明显比默认采集大很多。采集时间要控制得短一些,生产环境中应避免长时间的 ThreadTime 采集。
7. 与 EventSource 组合使用 ── 用业务语言切分“哪个区间慢”
CPU Stacks 和 Thread Time 都是整个进程的汇总结果。如果想按业务处理的粒度切分,比如“一笔订单处理中哪一步慢”,上一篇文章中提到的 EventSource 的 Start/Stop 事件就能发挥作用。
- 在应用侧埋点,记录类似
OrderProcessingStart/OrderProcessingStop的事件(实现方式请参考上一篇文章) - 在 PerfView 的“Events”视图中确认该事件的发生时刻,并把调用栈查看器的时间范围(Start/End 过滤器)收窄到该区间
- 在收窄后的范围内解读 CPU/阻塞时间的明细
这样就能只针对“那一次变慢”进行分析,不会被平时的噪声淹没。自定义事件同样可以用 dotnet-trace 采集,因此只要埋点一次,开发机和生产环境都能用相同的排查方法。性能排查的难度,从实务经验来看,往往取决于“应用侧是否有观测点”,而不是操作工具的熟练程度。
总结
- 出发点是把“变慢”区分为 CPU bound 和阻塞两类。前者只有 CPU 采样(1 个样本≈1ms)才能看到,后者只有 ThreadTime 采集才能看到。
- 基本分工是:先用不需要管理员权限的 dotnet-trace,需要整台机器・原生代码・阻塞时间时再用 PerfView。采集完的
.nettrace再用 PerfView 读取,这种组合方式也是常规操作。 - 调用栈查看器的核心解读方式在于区分 exclusive 与 inclusive,并始终要确认样本数是否充足。
- 提前用 EventSource 自定义事件切出业务处理的边界,能让排查的精确度再提升一个层次。
合同会社小村软件(KomuraSoft)承接“变慢”“卡死”“CPU 占满”等 Windows 业务应用性能问题的排查,以及为了便于测量而进行的埋点・设计咨询。如果已经具备复现步骤和跟踪文件,也可以只委托后续的分析工作。
相关文章
- Windows 事件日志・ETW 入门 ── 把业务应用的日志接入操作系统的标准机制
- 在 .NET 中区分 GC 等待与内存泄漏 ── 观测、比较、证明持续增长的内存的实务流程
- 用 WinDbg + SOS 读取崩溃转储 ── 采集之后的实务分析入门
- Windows 崩溃转储采集入门 - WER/ProcDump/WinDbg
- 什么是 PDB(程序数据库) ── 理解调试信息、符号与 Source Link
- 在 Windows 上正确比较不同版本程序速度的方法
- 用一张表梳理 WPF/WinForms 的 async 与 UI 线程
相关咨询领域
参考链接
-
GitHub, PerfView User’s Guide. 说明默认采集是每个处理器每 1 毫秒间隔的 CPU 样本(含调用栈)、判断时建议采集 1,000~5,000 个左右的样本、以及 Thread Time 选项可以采集上下文切换以便分析阻塞时间(也可从 PerfView 自带的 Help 中查阅)。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, EventPipe Overview. 说明 EventPipe 是一种不依赖管理员权限等高权限组件即可跟踪 .NET 应用的机制,以及其范围仅限于托管代码与运行时,无法获取内核事件或原生调用栈。 ↩ ↩2
-
Microsoft Learn, Collect and View EventSource Traces. 说明 ETW 跟踪采集始终需要管理员权限,以及 PerfView 与 Visual Studio 都能打开 .nettrace 文件。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, dotnet-counters diagnostic tool. 说明通过 System.Runtime 计数器监控运行中进程的 CPU、GC、ThreadPool 等指标。 ↩
-
Microsoft Learn, dotnet-trace diagnostic tool. 说明 collect 命令的用法、默认启用的配置文件(dotnet-common / dotnet-sampled-thread-time)与 cpu-sampling 配置文件的废弃、gc-verbose / gc-collect 等配置文件,以及转换为 Speedscope/Chromium 格式。 ↩ ↩2 ↩3
-
GitHub, microsoft/perfview. 说明 PerfView 是用于排查 CPU、内存相关性能问题的免费性能分析工具,以及可以直接从发布页面获取可执行文件。 ↩
-
GitHub, microsoft/perfview Issue #598. 说明 PerfView 维护者关于默认采集时开销大致在 3% 左右的解释,以及可以用 /CpuSampleMSec 调整采样间隔以降低负载。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
Windows 业务应用程序的日志,是不是只靠文件日志来解决?事件日志与 ETW,是运维人员和操作系统标准工具所能看到的另一层记录。本文整理三种手段的分工方式、.NET 中的写入方法、通过 EventSource 进行的 ETW 埋点、收集与排查的实务做法,以及来源未注册、...
用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
本文说明如何用 WinDbg 与 SOS 扩展实际读取已采集的 Windows 崩溃转储。内容涵盖符号路径设置、以 !clrstack、!pe、!dumpheap -stat、!gcroot 追踪异常与内存泄漏的方法、原生崩溃的 !analyze -v,以及与 dotnet...
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
Windows 业务应用程序的打印与 PDF 输出 ── System.Drawing.Printing / WPF / 报表库的取舍选型
本文按需求整理成判断表,梳理 PrintDocument 的 WinForms 打印、WPF 的 FlowDocument/FixedDocument 打印,以及 PDF 输出的可选方案。并从实务角度讲解分页控制、DPI 偏差等实现陷阱,以及从 Windows 服务打印、依...
在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)
本文整理在 C# 中通过 P/Invoke 调用 Win32 API 或原生 DLL 时的实务要点:DllImport 与 LibraryImport 的区别、用 CsWin32 自动生成签名、字符串封送的陷阱、用 SafeHandle 管理句柄、SetLastError ...
常见问题
汇总了咨询这一主题时常见的问题。
- PerfView 和 dotnet-trace,该用哪一个?
- 建议先从 dotnet-trace 开始尝试。它不需要管理员权限即可使用,可以只针对目标进程进行采集,操作也只需一行命令就能完成。另一方面,如果想看到原生代码的调用栈、想看整台机器的情况(其他进程或内核的影响),或是想以上下文切换为单位追踪阻塞时间,就需要基于 ETW 的 PerfView。把采集到的 .nettrace 文件用 PerfView 打开分析,这种搭配使用方式也很常见。
- 和 Visual Studio 的性能分析器是什么关系?
- 如果问题能在开发机上复现,Visual Studio 的 CPU 使用率工具或内存工具会更方便,先用这些通常就足够了。PerfView 或 dotnet-trace 能发挥作用的场景,是在无法安装 Visual Studio 的测试机、生产机上采集数据时,想把采集和分析拆分到不同机器上时,或是想连 ETW 事件(自定义 EventSource 或内核事件)也一起查看时。另外,Visual Studio 也能打开 dotnet-trace 输出的 .nettrace 文件。
- 在生产服务器上运行也没问题吗?
- 这两个工具都是以在生产环境中短时间采集为前提设计的,但并非毫无条件。请按以下步骤操作:把采集时间控制在几十秒到几分钟之内,先在测试环境用同样的命令试运行以确认开销和文件大小,并确认磁盘有足够的可用空间。尤其是 ThreadTime(上下文切换)采集或分配跟踪,事件量很大,文件会迅速变大。此外,跟踪内容中会包含命令行参数等信息,因此对采集到的文件也需要谨慎处理。
- WPR/WPA 该如何区分使用?
- WPR(Windows Performance Recorder)和 WPA(Windows Performance Analyzer)是基于同样的 ETW、更偏向操作系统层面的排查工具。在磁盘 I/O、电源、启动时间等操作系统整体的详细分析上,WPR/WPA 更强;而在 .NET 应用的 CPU、GC、阻塞时间排查上,针对托管代码显示做了优化的 PerfView 更易读。如果目的是排查业务应用问题,PerfView 与 dotnet-trace 基本上就够用了。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。