• 作者:老汪软件技巧
  • 发表时间:2024-08-23 17:01
  • 浏览量:

项目进程所占用内存随运行时间缓慢增长,

首先怀疑是垃圾回收不积极的问题尝试在docker中运行,并设置内存上限,结果进程OOM退出配置如DOTNET_GCHeapHardLimitPercent、DOTNET_GCConserveMemory、DOTNET_GCHeapHardLimit等参数,控制堆大小,加强垃圾回收强度,结果仍溢出

说明不是垃圾回收不积极的问题

然后怀疑业务代码有问题

经简单排查,没有明显的业务代码逻辑问题,如一直往List中添加元素不清除这种问题

之后进行监控和分析

使用dotmemory附加进程,隔一段时间打一个dump,每两次dump进行对比,发现业务逻辑之外的对象中,Dictionary和ServiceProviderEngineScope不断增长,几乎不回收

查看对象个数,二者相同,查看引用关系,Dictionary被ServiceProviderEngineScope,说明二者是同一处内存泄漏点导致的,继续向上查找引用,找不到业务代码,因此怀疑是使用依赖注入框架导致的,在获取对象实例时,创建了Scope,而没有进行回收。

项目中注册的大部分对象都是单例的,应该不会创建scope;项目也没有注册Scoped对象,为什么会创建scope呢?想来想去,难道是Transient对象导致的?因为项目中确实有一处使用到了瞬态对象,且根据业务逻辑,会在后台线程中频繁获取。

将此处的Transient对象改成单例进行试验

由于此处的瞬态对象的字段中不保存状态,进注入其他单例对象,故改成单例不会引发错误,于是将其改成单例,重新使用dotmemory启动进程进行监控,果然,问题解决,ServiceProviderEngineScope不再增长

原因分析

问题虽然解决了,但是感觉还是不太对劲,Transient对象不像Scoped对象有作用域范围,应该是每次找容器要的时候都new一个新的出来,为什么会创建出这个scope出来呢?

且监控项目内存的过程中,还发现一个现象,项目在后台运行很长时间,没有前端接口请求时,占用内存不断增长,但是前端只要一请求,就会进行大量的内存回收:

所以说,难道这个scope的回收还和接口调用有关么?

靠猜是没有用的,只好把源码拿过来调查

内存泄漏检查_内存泄漏js_

源码调查

项目使用的是furion框架,相当于在微软的上边包装了一层,因此Transient对象不是用原生的AddTransient()的方式注册的,而是标记的ITransient接口声明,之后获取时,也是用furion封装的App.GetService()这种静态方法拿到的

于是把源码clone过来,通过实现ITransient接口注册一个Transient对象,然后通过App.GetService拿到这个对象,打个断点,看看到底走了哪些代码

一路追踪,发现走到了这里:

果然,这里通过RootServices.CreateScope()创建了scope,并且通过UnmanagedObjects.Add(scoped)将这个scope保存了起来

那么为什么调用接口就会释放内存呢?查找UnmanagedObjects的引用,发现这里有个Clear()方法被调用了

是在这里被调用的:

这个外层的DisposeUnmanagedObjects方法在哪里被调用的呢?答案是中间件:

这就解释了请求接口释放内存的疑问了,请求接口触发了这个中间件,所以clear了存scope的List,他们就能被垃圾回收了。

进一步观察,这个DisposeUnmanagedObjects方法是public的,所以说在合适的时机去调这个方法,应该也能达到同样的效果

再仔细看文档,发现有关于这个ITransient接口的说明在处理请求的应用中,在请求结束时会释放暂时服务。,所以说,这东西的设计思路和的瞬态对象不是一个概念,本质上也是一个scope作用域的东西,只是作用域是单次请求

再撸源代码 + 查阅文档,发现可以从App.RootServices拿到根容器,所以通过App.RootServices.GetService()来获取实例就不会创建scope了,或者使用重载App.GetService(App.RootServices),把App.RootServices传进去,也不会创建scope了

总结

用框架要小心,多看文档和源码,尽量别望文生义,不然遇到反直觉的设计的时候,就容易掉进坑里