.NET性能分析 dotnet-profiling

.NET 性能分析技能用于诊断和优化 .NET 应用程序的性能问题,通过实时监控、事件追踪和内存转储分析工具,帮助开发人员识别 CPU 热点、内存泄漏和 GC 压力。关键词:.NET、性能分析、dotnet-counters、dotnet-trace、dotnet-dump、火焰图、内存转储、GC 指标。

后端开发 0 次安装 0 次浏览 更新于 3/6/2026

名称: 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-counttime-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

阅读火焰图

火焰图显示调用堆栈,其中:

  • 宽度 表示在该函数中花费的总样本时间比例(越宽 = 越多时间)
  • 高度 表示调用堆栈深度(更高的堆栈 = 更深的调用链)
  • 颜色 通常是任意的(无意义),除非工具使用特定的颜色方案

分析工作流:

  1. 寻找 宽阔的高原 – 消耗大量样本比例的函数
  2. 跟随最宽的帧 向上 找到哪些调用者贡献最多时间
  3. 识别 意外的宽度 – 应该快速的框架方法显得宽阔表示误用
  4. 比较 前后 追踪以验证优化减少了目标函数的宽度

.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[]

分析方法:

  1. 寻找应用程序类型的意外高计数或大小
  2. 比较计数与预期基数(例如,2,100 个 OrderEntity 对象——在当前负载下是否预期?)
  3. 大的 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 显示当前线程的托管调用堆栈 关联线程状态与堆数据

内存泄漏调查工作流

  1. 基线: 在应用程序启动和初始预热后捕获转储
  2. 负载: 运行怀疑泄漏的工作负载场景
  3. 比较: 在工作负载完成后捕获第二个转储
  4. 差异: 比较两个转储之间的 dumpheap -stat 输出——寻找计数或总大小显著增长的类型
  5. 根: 在增长类型的实例上使用 gcroot 找到保留链
  6. 修复: 断开保留链(从静态集合移除、处置事件订阅、修复异步生命周期问题)
# 提示:保存 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] 创建目标基准,量化修复带来的改进。


代理陷阱

  1. 从 dotnet-counters 开始,而不是 dotnet-trace – 计数器开销几乎为零,并识别问题类别(CPU、内存、线程)。仅在计数器缩小调查范围后才转向追踪或转储。
  2. 在生产中使用 CPU 采样(不是仪表化) – 采样开销为 2-5%,对生产安全。仪表化增加 10-50%+ 开销,应限于开发环境。
  3. 始终将追踪转换为火焰图进行分析 – 读取原始的 .nettrace 事件日志不切实际。使用 dotnet-trace convert --format Speedscope 并在 https://www.speedscope.app/ 中打开进行视觉分析。
  4. 为泄漏调查捕获两个转储 – 单个转储显示当前状态但无法区分正常常驻对象与泄漏对象。比较在怀疑泄漏场景前后捕获的两个转储的堆统计。
  5. 使用 -min 85000 过滤 dumpheap 以找到 LOH 对象 – 对象 >= 85,000 字节进入大对象堆,仅在 Gen 2 GC 中收集。大的 LOH 计数表示潜在的碎片化。
  6. 使用 [skill:dotnet-observability] 解释 GC 计数器数据 – 运行时 GC/线程池计数器与 OpenTelemetry 指标重叠。使用可观察性技能关联剖析发现与分布式追踪上下文。
  7. 不要混淆 dotnet-trace gc-collect 与 dotnet-dump – gc-collect 追踪时间上的分配事件(哪些方法分配);dotnet-dump 捕获 point-in-time 堆快照(存在哪些对象)。使用 gc-collect 进行分配率分析;使用 dotnet-dump 进行保留/泄漏分析。