用 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-commondotnet-sampled-thread-time5 根据用途,也可以选择 --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 业务应用性能问题的排查,以及为了便于测量而进行的埋点・设计咨询。如果已经具备复现步骤和跟踪文件,也可以只委托后续的分析工作。

相关文章

相关咨询领域

参考链接

  1. GitHub, PerfView User’s Guide. 说明默认采集是每个处理器每 1 毫秒间隔的 CPU 样本(含调用栈)、判断时建议采集 1,000~5,000 个左右的样本、以及 Thread Time 选项可以采集上下文切换以便分析阻塞时间(也可从 PerfView 自带的 Help 中查阅)。  2 3 4 5

  2. Microsoft Learn, EventPipe Overview. 说明 EventPipe 是一种不依赖管理员权限等高权限组件即可跟踪 .NET 应用的机制,以及其范围仅限于托管代码与运行时,无法获取内核事件或原生调用栈。  2

  3. Microsoft Learn, Collect and View EventSource Traces. 说明 ETW 跟踪采集始终需要管理员权限,以及 PerfView 与 Visual Studio 都能打开 .nettrace 文件。  2 3 4

  4. Microsoft Learn, dotnet-counters diagnostic tool. 说明通过 System.Runtime 计数器监控运行中进程的 CPU、GC、ThreadPool 等指标。 

  5. Microsoft Learn, dotnet-trace diagnostic tool. 说明 collect 命令的用法、默认启用的配置文件(dotnet-common / dotnet-sampled-thread-time)与 cpu-sampling 配置文件的废弃、gc-verbose / gc-collect 等配置文件,以及转换为 Speedscope/Chromium 格式。  2 3

  6. GitHub, microsoft/perfview. 说明 PerfView 是用于排查 CPU、内存相关性能问题的免费性能分析工具,以及可以直接从发布页面获取可执行文件。 

  7. GitHub, microsoft/perfview Issue #598. 说明 PerfView 维护者关于默认采集时开销大致在 3% 左右的解释,以及可以用 /CpuSampleMSec 调整采样间隔以降低负载。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表