名称: dotnet-profiling 描述: “诊断 .NET 性能问题。dotnet-counters、dotnet-trace、dotnet-dump、火焰图。” 用户可调用: false
dotnet-profiling
诊断 .NET 性能问题的工具指南。包括使用 dotnet-counters 进行实时指标监控、使用 dotnet-trace 进行事件追踪和火焰图生成,以及使用 dotnet-dump 进行内存转储捕获和分析。侧重于解释剖析数据(阅读火焰图、分析堆转储、关联 GC 指标)而不仅仅是调用工具。
版本假设: .NET SDK 8.0+ 基线。所有三种诊断工具(dotnet-counters、dotnet-trace、dotnet-dump)随 .NET SDK 提供——无需单独安装。
范围
- 使用 dotnet-counters 进行实时指标监控
- 使用 dotnet-trace 进行事件追踪和火焰图生成
- 使用 dotnet-dump 进行内存转储捕获和分析
- 解释剖析数据(火焰图、堆转储、GC 指标)
超出范围
- OpenTelemetry 指标和分布式追踪——参见 [skill:dotnet-observability]
- 微基准测试设置(BenchmarkDotNet)——参见 [skill:dotnet-benchmarkdotnet]
- 性能架构模式(Span<T>、ArrayPool、密封)——参见 [skill:dotnet-performance-patterns]
- CI 中的持续基准回归检测——参见 [skill:dotnet-ci-benchmarking]
- 架构模式(缓存、弹性)——参见 [skill:dotnet-architecture-patterns]
交叉引用: [skill:dotnet-observability] 用于 GC/线程池指标解释和 OpenTelemetry 关联,[skill:dotnet-benchmarkdotnet] 用于在剖析识别热路径后进行结构化基准测试,[skill:dotnet-performance-patterns] 用于基于剖析结果应用的优化模式。
dotnet-counters – 实时指标监控
概述
dotnet-counters 提供 .NET 运行时指标的实时监控,无需修改应用程序代码。使用它作为首次分类工具,在转向更重的仪表化之前识别性能问题是 CPU 绑定、内存绑定还是 I/O 绑定。
监控运行进程
# 列出运行中的 .NET 进程
dotnet-counters ps
# 监控进程的默认运行时计数器
dotnet-counters monitor --process-id <PID>
# 使用特定的刷新间隔(秒)监控
dotnet-counters monitor --process-id <PID> --refresh-interval 2
关键内置计数器提供程序
| 提供程序 | 计数器 | 它告诉你什么 |
|---|---|---|
System.Runtime |
CPU 使用率、GC 堆大小、Gen 0/1/2 回收、线程池队列长度、异常计数 | 整体运行时健康状况 |
Microsoft.AspNetCore.Hosting |
请求率、请求持续时间、活动请求 | HTTP 请求吞吐量和延迟 |
Microsoft.AspNetCore.Http.Connections |
连接持续时间、当前连接 | WebSocket/SignalR 连接负载 |
System.Net.Http |
请求开始/失败、活动请求、连接池大小 | 出站 HTTP 客户端行为 |
System.Net.Sockets |
发送/接收的字节数、数据报、连接 | 网络 I/O 量 |
监控特定提供程序
# 一起监控运行时和 ASP.NET 计数器
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime,Microsoft.AspNetCore.Hosting
# 仅监控 GC 相关计数器
dotnet-counters monitor --process-id <PID> \
--counters System.Runtime[gc-heap-size,gen-0-gc-count,gen-1-gc-count,gen-2-gc-count]
自定义 EventCounters
应用程序可以发布自定义计数器用于领域特定指标:
using System.Diagnostics.Tracing;
[EventSource(Name = "MyApp.Orders")]
public sealed class OrderMetrics : EventSource
{
public static readonly OrderMetrics Instance = new();
private EventCounter? _orderProcessingTime;
private IncrementingEventCounter? _ordersProcessed;
private OrderMetrics()
{
_orderProcessingTime = new EventCounter("order-processing-time", this)
{
DisplayName = "订单处理时间 (ms)",
DisplayUnits = "ms"
};
_ordersProcessed = new IncrementingEventCounter("orders-processed", this)
{
DisplayName = "处理的订单",
DisplayRateTimeScale = TimeSpan.FromSeconds(1)
};
}
public void RecordProcessingTime(double milliseconds)
=> _orderProcessingTime?.WriteMetric(milliseconds);
public void RecordOrderProcessed()
=> _ordersProcessed?.Increment();
protected override void Dispose(bool disposing)
{
_orderProcessingTime?.Dispose();
_ordersProcessed?.Dispose();
base.Dispose(disposing);
}
}
监控自定义计数器:
dotnet-counters monitor --process-id <PID> --counters MyApp.Orders
解释计数器数据
使用计数器值指导进一步调查。参见 [skill:dotnet-observability] 用于关联这些运行时指标与 OpenTelemetry 追踪:
| 症状 | 计数器证据 | 下一步 |
|---|---|---|
| 高 CPU 使用率 | cpu-usage > 80%,threadpool-queue-length 低 |
使用 dotnet-trace 进行 CPU 剖析 |
| 内存增长 | gc-heap-size 增加,频繁的 Gen 2 GC |
使用 dotnet-dump 进行内存转储 |
| 线程饥饿 | threadpool-queue-length 增长,threadpool-thread-count 达到最大 |
检查同步异步或阻塞调用 |
| 请求延迟 | request-duration 高,active-requests 正常 |
使用 dotnet-trace 追踪单个请求 |
| GC 暂停 | 高 gen-2-gc-count,time-in-gc > 10% |
使用 dotnet-trace gc-collect 进行分配剖析 |
导出计数器数据
# 导出到 CSV 进行分析
dotnet-counters collect --process-id <PID> \
--format csv \
--output counters.csv \
--counters System.Runtime
# 导出到 JSON 用于程序化消费
dotnet-counters collect --process-id <PID> \
--format json \
--output counters.json
dotnet-trace – 事件追踪和火焰图
概述
dotnet-trace 从运行中的 .NET 进程捕获详细的事件追踪。追踪可以作为火焰图分析以识别 CPU 热路径,或配置用于分配追踪以找到 GC 压力来源。
CPU 采样
CPU 采样以固定间隔记录堆栈帧,以构建应用程序花费时间的统计配置文件:
# 收集 CPU 采样追踪(默认配置文件)
dotnet-trace collect --process-id <PID> --duration 00:00:30
# 使用 cpu-sampling 配置文件收集(明确)
dotnet-trace collect --process-id <PID> \
--profile cpu-sampling \
--output cpu-trace.nettrace
CPU 采样 vs 仪表化
| 方法 | 开销 | 最佳用途 | 工具 |
|---|---|---|---|
| CPU 采样 | 低 (~2-5%) | 在生产环境中找到 CPU 热路径 | dotnet-trace --profile cpu-sampling |
| 仪表化 | 高 (10-50%+) | 确切的调用计数、方法进入/退出计时 | Rider/VS 剖析器、PerfView |
CPU 采样由于开销低,适合生产使用。将其作为默认方法。在开发环境中保留仪表化,用于确切的调用计数。
火焰图生成
追踪文件(.nettrace)必须转换为火焰图格式以进行视觉分析:
使用 Speedscope(基于浏览器,推荐):
# 转换为 Speedscope 格式
dotnet-trace convert cpu-trace.nettrace --format Speedscope
# 打开 cpu-trace.speedscope.json -- 在 https://www.speedscope.app/ 加载
使用 PerfView(Windows,深度 .NET 集成):
# 转换为 Chromium 追踪格式(也可在 chrome://tracing 查看)
dotnet-trace convert cpu-trace.nettrace --format Chromium
阅读火焰图
火焰图显示调用堆栈,其中:
- 宽度 表示在该函数中花费的总样本时间比例(越宽 = 越多时间)
- 高度 表示调用堆栈深度(更高的堆栈 = 更深的调用链)
- 颜色 通常是任意的(无意义),除非工具使用特定的颜色方案
分析工作流:
- 寻找 宽阔的高原 – 消耗大量样本比例的函数
- 跟随最宽的帧 向上 找到哪些调用者贡献最多时间
- 识别 意外的宽度 – 应该快速的框架方法显得宽阔表示误用
- 比较 前后 追踪以验证优化减少了目标函数的宽度
.NET 火焰图中的常见模式:
| 模式 | 可能原因 | 调查 |
|---|---|---|
宽阔的 System.Linq 帧 |
LINQ 重的热路径与委托开销 | 用 foreach 循环或基于 Span 的处理替换 |
宽阔的 JIT_New / gc_heap::allocate |
触发 GC 的过度分配 | 使用 --profile gc-collect 进行分配剖析 |
宽阔的 Monitor.Enter / SpinLock |
锁竞争 | 审查同步策略 |
宽阔的 System.Text.RegularExpressions |
正则表达式回溯 | 使用 RegexOptions.NonBacktracking 或编译正则表达式 |
| 深的异步状态机帧 | 紧循环中的异步开销 | 考虑 CPU 绑定工作的同步路径 |
使用 gc-collect 配置文件进行分配追踪
gc-collect 配置文件捕获分配事件以识别哪些代码路径分配最多内存:
# 收集分配数据
dotnet-trace collect --process-id <PID> \
--profile gc-collect \
--duration 00:00:30 \
--output alloc-trace.nettrace
这产生一个追踪,显示:
- 哪些方法分配最多字节
- 哪些类型最频繁分配
- 分配大小和触发它们的调用堆栈
将分配数据与 dotnet-counters 的 GC 计数器证据关联。如果 gen-2-gc-count 高,分配追踪显示哪些代码路径产生存活到 Gen 2 的长期对象。参见 [skill:dotnet-performance-patterns] 用于在识别热分配站点后应用的零分配模式。
自定义追踪提供程序
针对特定事件提供程序进行聚焦追踪:
# 使用关键词和详细程度追踪特定提供程序
dotnet-trace collect --process-id <PID> \
--providers "Microsoft-Diagnostics-DiagnosticSource:::FilterAndPayloadSpecs=[AS]System.Net.Http"
# 追踪 EF Core 查询(与 [skill:dotnet-efcore-patterns] 一起有用)
dotnet-trace collect --process-id <PID> \
--providers Microsoft.EntityFrameworkCore
# 追踪 ASP.NET Core 请求处理
dotnet-trace collect --process-id <PID> \
--providers Microsoft.AspNetCore
追踪文件管理
| 格式 | 扩展名 | 查看器 | 跨平台 |
|---|---|---|---|
| NetTrace | .nettrace |
PerfView、VS、dotnet-trace convert | 是(捕获);Windows(PerfView) |
| Speedscope | .speedscope.json |
https://www.speedscope.app/ | 是 |
| Chromium | .chromium.json |
Chrome DevTools(chrome://tracing) | 是 |
dotnet-dump – 内存转储分析
概述
dotnet-dump 捕获和分析进程内存转储。使用它来调查内存泄漏、大对象堆碎片化和对象引用链。与 dotnet-trace 不同,转储捕获整个托管堆的 point-in-time 快照。
捕获转储
# 捕获完整的堆转储
dotnet-dump collect --process-id <PID> --output app-dump.dmp
# 捕获最小转储(更快、更小,但细节较少)
dotnet-dump collect --process-id <PID> --type Mini --output app-mini.dmp
何时捕获:
- 内存使用超过预期基线(与 dotnet-counters
gc-heap-size比较) - 应用程序接近 OOM 条件
- 在负载测试后怀疑内存泄漏
- 调查终结器队列积压
使用 SOS 命令分析转储
在交互式分析器中打开转储:
dotnet-dump analyze app-dump.dmp
!dumpheap – 堆对象摘要
列出托管堆上的对象,按类型分组,按总大小排序:
> dumpheap -stat
统计:
MT 计数 总大小 类名
00007fff2c6a4320 125 4,000 System.String[]
00007fff2c6a1230 8,432 269,824 System.String
00007fff2c7b5640 2,100 504,000 MyApp.Models.OrderEntity
00007fff2c6a0988 15,230 1,218,400 System.Byte[]
分析方法:
- 寻找应用程序类型的意外高计数或大小
- 比较计数与预期基数(例如,2,100 个 OrderEntity 对象——在当前负载下是否预期?)
- 大的
System.Byte[]计数通常表示无界缓冲或流处理问题
按类型过滤:
> dumpheap -type MyApp.Models.OrderEntity
> dumpheap -type System.Byte[] -min 85000
-min 85000 过滤器显示大对象堆条目(对象 >= 85,000 字节,导致 Gen 2 GC 压力)。
!gcroot – 查找对象保留
追踪从 GC 根到特定对象的引用链,解释为什么它没有被收集:
> gcroot 00007fff3c4a2100
句柄表:
00007fff3c010010 (强句柄)
-> 00007fff3c3a1000 MyApp.Services.CacheService
-> 00007fff3c3a1020 System.Collections.Generic.Dictionary`2
-> 00007fff3c4a2100 MyApp.Models.OrderEntity
找到 1 个唯一根。
常见根类型及其含义:
| 根类型 | 含义 | 可能问题 |
|---|---|---|
strong handle |
静态字段或 GC 句柄 | 静态集合增长没有驱逐 |
pinned handle |
为本地交互固定 | 缓冲区固定时间超过需要 |
async state machine |
在异步闭包中捕获 | 长运行异步操作持有引用 |
finalizer queue |
等待终结器线程 | 终结器积压阻塞收集 |
threadpool |
从线程本地存储引用 | 线程静态缓存没有清理 |
!finalizequeue – 终结器队列分析
显示等待终结的对象,这延迟它们的收集至少一个 GC 周期:
> finalizequeue
要清理的 SyncBlocks:0
要释放的自由线程接口:0
要释放的 MTA 接口:0
要释放的 STA 接口:0
----------------------------------
第 0 代有 12 个可终结对象
第 1 代有 45 个可终结对象
第 2 代有 230 个可终结对象
准备终结 8 个对象
关键指标:
- “准备终结”计数高意味着终结器线程落后
- 第 2 代可终结列表中的对象成本高——它们至少存活两个 GC 周期(一个安排终结,一个在终结后收集)
- 实现
~Destructor()而没有调用IDisposable.Dispose()的类型是主要原因
堆分析的其他 SOS 命令
| 命令 | 目的 | 何时使用 |
|---|---|---|
dumpobj <address> |
显示特定对象的字段值 | 在用 dumpheap 找到对象后检查其状态 |
dumparray <address> |
显示数组内容 | 调查在堆统计中发现的大数组 |
eeheap -gc |
显示 GC 堆段布局 | 调查 LOH 碎片化 |
gcwhere <address> |
显示哪个 GC 代持有对象 | 确定对象是否固定或在中 LOH |
dumpmt <MT> |
显示方法表详情 | 调查类型元数据 |
threads |
列出所有托管线程及其堆栈追踪 | 识别死锁或阻塞 |
clrstack |
显示当前线程的托管调用堆栈 | 关联线程状态与堆数据 |
内存泄漏调查工作流
- 基线: 在应用程序启动和初始预热后捕获转储
- 负载: 运行怀疑泄漏的工作负载场景
- 比较: 在工作负载完成后捕获第二个转储
- 差异: 比较两个转储之间的
dumpheap -stat输出——寻找计数或总大小显著增长的类型 - 根: 在增长类型的实例上使用
gcroot找到保留链 - 修复: 断开保留链(从静态集合移除、处置事件订阅、修复异步生命周期问题)
# 提示:保存 dumpheap 输出用于比较
# 在转储 1 中:
> dumpheap -stat > /tmp/heap-before.txt
# 在转储 2 中:
> dumpheap -stat > /tmp/heap-after.txt
# 外部比较:
# diff /tmp/heap-before.txt /tmp/heap-after.txt
剖析工作流摘要
使用诊断工具进行结构化调查工作流:
1. dotnet-counters (分类)
├── CPU 高? → dotnet-trace --profile cpu-sampling
│ → 转换为火焰图 (Speedscope)
│ → 识别热方法
├── 内存增长? → dotnet-dump collect
│ → dumpheap -stat (找到大/多的类型)
│ → gcroot (找到保留链)
│ → 修复保留 + 用第二个转储验证
├── GC 压力? → dotnet-trace --profile gc-collect
│ → 识别分配热路径
│ → 应用零分配模式 [skill:dotnet-performance-patterns]
└── 线程饥饿? → dotnet-dump analyze
→ threads (列出所有托管线程)
→ clrstack (检查阻塞调用)
在剖析识别瓶颈后,使用 [skill:dotnet-benchmarkdotnet] 创建目标基准,量化修复带来的改进。
代理陷阱
- 从 dotnet-counters 开始,而不是 dotnet-trace – 计数器开销几乎为零,并识别问题类别(CPU、内存、线程)。仅在计数器缩小调查范围后才转向追踪或转储。
- 在生产中使用 CPU 采样(不是仪表化) – 采样开销为 2-5%,对生产安全。仪表化增加 10-50%+ 开销,应限于开发环境。
- 始终将追踪转换为火焰图进行分析 – 读取原始的
.nettrace事件日志不切实际。使用dotnet-trace convert --format Speedscope并在 https://www.speedscope.app/ 中打开进行视觉分析。 - 为泄漏调查捕获两个转储 – 单个转储显示当前状态但无法区分正常常驻对象与泄漏对象。比较在怀疑泄漏场景前后捕获的两个转储的堆统计。
- 使用
-min 85000过滤 dumpheap 以找到 LOH 对象 – 对象 >= 85,000 字节进入大对象堆,仅在 Gen 2 GC 中收集。大的 LOH 计数表示潜在的碎片化。 - 使用 [skill:dotnet-observability] 解释 GC 计数器数据 – 运行时 GC/线程池计数器与 OpenTelemetry 指标重叠。使用可观察性技能关联剖析发现与分布式追踪上下文。
- 不要混淆 dotnet-trace gc-collect 与 dotnet-dump – gc-collect 追踪时间上的分配事件(哪些方法分配);dotnet-dump 捕获 point-in-time 堆快照(存在哪些对象)。使用 gc-collect 进行分配率分析;使用 dotnet-dump 进行保留/泄漏分析。