1. 高并发性能问题定位
高并发场景下的性能问题往往不是单一原因造成的。一个接口在压测中响应时间从 20ms 涨到 800ms,可能是线程池被同步 I/O 阻塞,也可能是 GC 频繁触发导致 STW 停顿,还可能是磁盘日志写入拖慢了请求处理,甚至只是网卡带宽被打满。如果只盯着一个指标排查,很容易在错误的方向上浪费大量时间。因此,性能定位的第一步不是"优化",而是建立可观测的基线——先知道系统在正常负载下长什么样,才能在异常出现时快速判断偏离发生在哪个维度。
四个维度各自回答什么问题
CPU、内存、磁盘、网络这四个维度并不是并列的四个"检查项",它们分别回答不同层面的问题:
- CPU:谁在消耗计算资源?是用户态的业务逻辑,还是内核态的系统调用?是否存在大量线程上下文切换?
- 内存:托管堆是否在持续增长?GC 的暂停时间是否已经影响到请求延迟?是否存在非托管内存泄漏?
- 磁盘:I/O 是否成为瓶颈?是日志写入、数据库访问还是文件读取在拖慢响应?
- 网络:连接数、吞吐量、延迟是否达到网卡或协议栈的上限?是否存在大量 TIME_WAIT 或重传?
这四个维度之间存在因果传导关系。例如,内存压力增大导致 GC 频繁,GC 占用 CPU 时间片,进而使请求处理变慢,客户端超时重试又增加了网络连接数。所以观测基线要同时采集四个维度,才能看清传导链条。
用 dotnet-counters 建立实时基线
dotnet-counters 是最轻量的实时观测工具,适合在压测过程中持续观察指标变化。它的核心用法是指定进程 ID 和监控的计数器提供程序:
dotnet-counters monitor --process-id 12345 \
--counters System.Runtime,Microsoft.AspNetCore.Hosting,Microsoft.AspNetCore.Server.Kestrel
这条命令会持续刷新以下关键指标:
cpu-usage:进程 CPU 占用百分比,反映整体计算压力。working-set:进程工作集大小,观察内存是否持续增长。gc-heap-size:GC 堆大小,判断托管内存趋势。gen-0-gc-count/gen-1-gc-count/gen-2-gc-count:各代 GC 触发次数。time-in-gc:GC 时间占比,超过 10% 就值得警惕。threadpool-queue-length:线程池队列长度,持续大于 0 说明线程池处理不过来。requests-per-second:每秒请求数,与延迟指标对照看。
在压测中,如果观察到 threadpool-queue-length 持续增长而 cpu-usage 并不高,通常意味着存在同步阻塞调用(如 .Result 或 .Wait()),线程被占满但 CPU 空闲。反之,如果 CPU 接近 100% 且 time-in-gc 也偏高,则要优先排查内存分配和 GC 配置。
用 dotnet-trace 采集详细事件
当 dotnet-counters 发现异常信号后,需要用 dotnet-trace 采集更细粒度的事件,定位到具体的方法和调用栈:
dotnet-trace collect --process-id 12345 \
--providers Microsoft-DotNETCore-SampleProfiler,Microsoft-Windows-DotNETRuntime:0x1:5 \
--duration 00:00:30
采集完成后会生成 .nettrace 文件,可以在 Visual Studio 中打开,或用 dotnet-trace convert 转换为 Speedscope 格式在浏览器中分析。重点关注:
- CPU 采样火焰图:哪些方法占用了最多 CPU 时间。
- GC 事件:每次 GC 的触发原因、暂停时长、回收前后堆大小。
- 线程池事件:线程注入速率、队列等待时间。
- 异常抛出:频繁抛异常会显著拖慢性能。
用 PerfView 做深度分析
PerfView 是 Windows 平台上最强大的性能分析工具,适合处理 .nettrace 和 .etl 文件。它的典型使用场景包括:
- 查看 GC 的详细统计,包括各代回收次数、暂停时间分布、LOH 分配情况。
- 分析线程池的线程注入和退休行为,判断是否存在线程饥饿。
- 查看磁盘 I/O 的调用栈,定位是哪个组件在频繁读写。
- 对比两次采集的差异,观察优化前后的变化。
在 PerfView 中打开 trace 文件后,常用视图包括 CPU Stacks(CPU 火焰图)、GC Stats(GC 统计)、Thread Time(线程时间分布)和 Events(原始事件列表)。对于高并发问题,建议先用 CPU Stacks 看整体热点,再用 GC Stats 确认内存压力,最后用 Thread Time 判断是否存在阻塞。
建立基线的实操建议
基线不是一次采集就能完成的,它需要覆盖不同的负载水平。建议按以下方式组织:
- 空载基线:应用启动后无请求时的指标,用于排除启动阶段的影响。
- 正常负载基线:按预期峰值 QPS 的 50% 压测 5 分钟,记录各维度指标范围。
- 峰值负载基线:按预期峰值 QPS 压测 10 分钟,观察指标是否稳定。
- 过载基线:按峰值 150% 压测,观察系统在压力下的退化行为。
每次采集都要记录时间戳,确保四个维度的数据可以对齐。例如,当发现 14:32:15 出现延迟尖峰时,可以回看同一时刻的 CPU、GC、磁盘 I/O 和网络指标,快速判断是哪个维度先出现异常。
常见异常信号速查
| 维度 | 异常信号 | 可能原因 | 下一步排查 |
|---|---|---|---|
| CPU | CPU > 80% 且线程池队列积压 | 计算密集型逻辑或同步阻塞 | dotnet-trace 火焰图定位热点方法 |
| 内存 | Gen2 GC 频繁且暂停 > 50ms | 大对象分配或内存泄漏 | dotnet-gcdump 分析堆对象 |
| 磁盘 | I/O 等待 > 20ms 或队列深度持续 > 2 | 日志写入过多或数据库慢查询 | PerfView 查看 I/O 调用栈 |
| 网络 | 连接数接近上限或 TIME_WAIT 激增 | 连接未复用或客户端重试 | 检查 HttpClient 复用和 Kestrel 配置 |
这张表不是用来"对号入座"的,而是帮助你在采集到数据后快速缩小排查范围。实际的高并发问题往往是多个维度叠加的结果,需要结合时间线做关联分析。建立基线的过程本身也是熟悉系统行为的过程,采集得越充分,后续定位问题的速度就越快。