Linux 下 .NET 程序 CPU 异常占用排查记录
问题背景
目标程序位于:/home/website_net/dev_web
现场现象:dotnet 进程 CPU 占用异常偏高,示例进程信息如下:
text
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1660968 root 20 0 24.4g 268004 69800 S 100.3 1.6 54:20.51 dotnet
初步判断:该现象通常不是正常状态,常见原因包括忙循环、异常风暴、线程池异常扩容、锁竞争、自旋、GC 压力过大等。
当前运行环境
执行命令:
bash
./dotnet --info
环境信息如下:
text
.NET SDK:
Version: 6.0.405
Commit: 27ab36058b
运行时环境:
OS Name: debian
OS Version: 11
OS Platform: Linux
RID: debian.11-arm64
Base Path: /home/website_net/netsdk/dotnet/sdk/6.0.405/
Host:
Version: 6.0.13
Architecture: arm64
.NET SDKs installed:
6.0.405 [/home/website_net/netsdk/dotnet/sdk]
.NET runtimes installed:
Microsoft.AspNetCore.App 6.0.13 [/home/website_net/netsdk/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 6.0.13 [/home/website_net/netsdk/dotnet/shared/Microsoft.NETCore.App]
结论:
- 当前机器是
Debian 11 arm64 - 当前 SDK 是
.NET 6.0.405 - 当前 Host Runtime 是
.NET 6.0.13 - 安装诊断工具时,必须选择兼容
.NET 6的版本
为什么安装 dotnet-trace 会失败
执行命令:
bash
./dotnet tool install --global dotnet-trace
报错:
text
error NU1202: 包 dotnet-trace 9.0.661903 与 net6.0 不兼容。
包 dotnet-trace 9.0.661903 支持: net8.0
原因:
- 默认安装到了最新的
dotnet-trace 9.x 9.x版本要求net8.0- 当前系统只有
.NET 6,因此出现NU1202不兼容错误
结论:
- 不是网络问题
- 不是工具名错误
- 是工具版本与本机运行时版本不匹配
正确安装方式
方式一:全局安装
先设置环境变量:
bash
export DOTNET_ROOT=/home/website_net/netsdk/dotnet
export PATH=$DOTNET_ROOT:$PATH
export PATH=$PATH:/root/.dotnet/tools
安装兼容 .NET 6 的版本:
bash
dotnet tool install --global dotnet-trace --version 6.*
dotnet tool install --global dotnet-stack --version 6.*
dotnet tool install --global dotnet-counters --version 6.*
验证:
bash
dotnet-trace --version
dotnet-stack --version
dotnet-counters --version
方式二:安装到固定目录
如果不希望使用全局目录,可以安装到指定路径:
bash
dotnet tool install dotnet-trace --tool-path /home/website_net/netsdk/dotnet/tools --version 6.*
dotnet tool install dotnet-stack --tool-path /home/website_net/netsdk/dotnet/tools --version 6.*
dotnet tool install dotnet-counters --tool-path /home/website_net/netsdk/dotnet/tools --version 6.*
验证:
bash
/home/website_net/netsdk/dotnet/tools/dotnet-trace --version
/home/website_net/netsdk/dotnet/tools/dotnet-stack --version
/home/website_net/netsdk/dotnet/tools/dotnet-counters --version
说明:
- 如果
6.*解析失败,可以改为指定精确的6.0.x版本 - 在无法访问
nuget.org的环境下,需要改用镜像源或离线包方式安装
推荐排查顺序
1. 先看是否某些线程打满
bash
top -H -p 1660968
目的:
- 确认是否单个或少数线程持续高 CPU
- 记录高 CPU 线程的 TID,便于后续分析
2. 看运行时指标
bash
dotnet-counters monitor -p 1660968 System.Runtime
重点关注:
cpu-usagegc-heap-sizegen-2-gc-countexception-countthreadpool-thread-countmonitor-lock-contention-count
判断方向:
exception-count很高:可能是异常反复抛出threadpool-thread-count快速增长:可能线程池饥饿或阻塞monitor-lock-contention-count高:可能锁竞争严重- GC 指标异常:可能对象分配过快、GC 压力大
3. 抓 CPU 采样,定位热点方法
bash
dotnet-trace collect -p 1660968 --profile cpu-sampling --duration 00:00:30 -o cpu.nettrace
目的:
- 找出 CPU 时间主要耗在哪些托管方法上
- 定位是否存在热点循环、频繁序列化、频繁日志、频繁 LINQ、正则、JSON 处理等问题
4. 连续查看托管线程栈
bash
dotnet-stack report -p 1660968
建议:
- 每隔 2 到 3 秒执行一次
- 如果多次看到同一批方法总在栈顶,基本可以判断热点位置
5. 如果怀疑 native 或内核层问题
bash
perf top -p 1660968
或者:
bash
perf record -g -p 1660968 -- sleep 30
perf report
目的:
- 看是否卡在
libc、futex、锁、自旋、系统库调用等位置
6. 如果怀疑系统调用风暴
bash
strace -fp 1660968 -tt -T
目的:
- 判断是否在高频执行
futex、epoll_wait、read、write等系统调用
常见根因清单
while循环没有sleep,形成忙等- 定时任务或
BackgroundService调度过于频繁 - 异常被捕获后无限重试,导致 CPU 被异常处理吃满
- 线程池饥饿,线程不断补充
- 锁竞争导致线程自旋
- 短周期大量分配对象,触发频繁 GC
- 日志打印过于频繁,尤其在循环内输出
- 某些缓存失效后反复全量扫描或重复计算
建议的最小排查命令清单
如果工具已安装,建议按下面顺序执行:
bash
top -H -p 1660968
dotnet-counters monitor -p 1660968 System.Runtime
dotnet-stack report -p 1660968
dotnet-trace collect -p 1660968 --profile cpu-sampling --duration 00:00:30 -o cpu.nettrace
perf top -p 1660968
strace -fp 1660968 -tt -T
现场结论摘要
- 当前环境是
.NET 6 arm64 - 直接安装最新版
dotnet-trace会失败,因为最新版需要.NET 8 - 需要明确指定
6.*版本安装dotnet-trace、dotnet-stack、dotnet-counters - 排查高 CPU 的首选方法是:
- 先看线程
- 再看 counters
- 再抓 stack
- 最后用 trace/perf 精确定位
后续建议
如果后续还要继续排查,可以把以下内容补充到这份文档中:
- 实际安装成功的工具版本号
dotnet-counters的实时输出截图或文本dotnet-stack的多次采样结果dotnet-trace分析出的热点方法- 最终定位到的具体代码位置和修复方案