.NET6 API高CPU占用排查:dotnet-trace定位System.Private.CoreLib问题
.NET6 API高CPU占用问题排查分析
我在排查部署于Kubernetes集群Docker容器中的.NET6 API高CPU问题,容器分配1000m CPU(1个完整vCPU)。应用逻辑仅包含2-3次数据库调用、若干外部API调用、JSON序列化及HttpClient调用,无重型计算,但仅处理约20次/秒请求(单请求耗时约100ms,仅2个并发请求)就耗尽1vCPU资源,触发Pod扩容,远低于预期承载能力。
通过dotnet-trace分析发现,自有代码CPU占比不足5%,System.Private.CoreLib中以下方法占据大量CPU资源,针对这些点做如下分析:
1. System.Reflection.Emit.DynamicMethod.CreateDelegate(占比~21%)
这个方法高占比确实和Newtonsoft.Json序列化高度相关。Newtonsoft.Json默认会为首次序列化的类型动态生成并编译序列化代码,正常情况下只会编译一次,之后复用生成的委托。但出现重复编译的话,会导致CPU飙升,常见触发场景:
- 序列化大量动态类型/匿名类型,每次实例的类型签名不同(比如不同方法中定义的匿名类型,或动态生成的类型)
- 存在大量不同的泛型类型实例化,且未被有效缓存
- 每次序列化都新建
JsonSerializerSettings实例,尤其是配置了TypeNameHandling等需要动态处理类型的选项时 - 程序集加载上下文频繁切换,导致缓存的委托失效
优化建议:
- 全局复用同一个
JsonSerializerSettings实例,不要每次序列化都新建 - 对频繁序列化的自定义类型,提前调用一次
JsonConvert.SerializeObject触发编译,完成缓存预热 - 排查是否存在大量动态/匿名类型序列化场景,替换为固定DTO类型
2. System.Runtime.Loader.AssemblyLoadContext.LoadFromAssemblyPath(占比~13%)
这个方法高占比明确说明应用在持续加载程序集。正常生产环境中,程序集应该在启动阶段就加载完成,不会持续触发加载。可能的原因:
- 代码中存在反射动态加载程序集的逻辑(比如
Assembly.LoadFrom、Activator.CreateInstance),且每次请求都重复加载 - 依赖注入容器配置不当,每次解析服务时都触发程序集加载
- 存在插件化逻辑,频繁加载新的插件程序集
- 某些第三方库内部存在重复加载程序集的问题
优化建议:
- 排查所有动态加载程序集的代码,确保只加载一次并缓存加载后的程序集实例
- 检查DI容器的注册方式,避免每次解析服务时触发反射加载
- 使用
dotnet-dump或更详细的dotnet-trace追踪,定位具体触发程序集加载的逻辑
3. System.Threading.LowLevelLifoSemaphore.WaitForSignal(占比~11%)
你当前的SemaphoreSlim用法是标准的正确写法:
private readonly SemaphoreSlim _mySemaphore = new SemaphoreSlim(1); (...) await _mySemaphore.WaitAsync(); (...) finally { _mySemaphore.Release(); }
正常情况下不会有这么高的CPU占比,出现这种情况的可能原因:
- 信号量的等待队列过长,大量请求在等待获取信号量,导致线程频繁上下文切换或自旋等待
WaitAsync和Release之间的代码执行时间过长,导致信号量长期被占用,后续请求阻塞等待- 实际并发请求数远超信号量允许的数量(你设置的是1,相当于单线程串行处理),即使平时只有2个并发,也可能存在瞬时突增的情况
优化建议:
- 评估业务是否真的需要单线程串行处理,若允许,适当提高信号量的初始计数(比如根据并发请求情况调整)
- 检查信号量保护范围内的代码,将耗时操作(如数据库查询、外部API调用)移出信号量保护逻辑,只保护真正需要串行的临界区
- 排查是否存在信号量未正确释放的场景(虽然当前写法正确,但可以检查是否有异常分支未进入
finally)
内容的提问来源于stack exchange,提问作者Maciej Pszczolinski
相关产品推荐
相关产品推荐

