【.Net Core】分析.net core在linux下内存占用过高问题--持续更新

现象

随着程序运行,内存占用率越来越高,直到触发linux的OOM,程序被杀死。

分析工具

运行环境:.net core 3.1(微软的分析工具要求最低3.0,无法分析2.1的core程序,需要先改为core 3.1才能分析)

linux:ubuntu 18

分析工具:dotnet-counters, dotnet-dump

工具的安装见:https://docs.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-counters

分析过程

1,获取要分析进程的pid

使用top或者ps等等工具,获取程序的pid

对于docker环境,如果没有安装top命令,可以使用如下安装

apt-get install procps

2,查看内存使用情况(我这里pid为13156)

dotnet-counters monitor -p 13156

 

 

从结果来看,GC中的Gen2占用了较多的内存,理论上,不应该有很多的Gen2,我们需要分析一下Gen2里面到底是什么?

Gen0,Gen1,Gen2以及LOH的区别,以及.net core内存管理机制,见:

https://docs.microsoft.com/en-us/aspnet/core/performance/memory?view=aspnetcore-5.0

3,获取进程的dump文件

dotnet-dump collect -p 13156

说明:要使用这条命令获取dump,如果在docker中,需要提供docker的--private参数,如果是在AWS的ECS中使用的Fargate模式运行,则不支持此参数。需要在EC2上运行。

此命令会在当前目录生成一个dump文件

4,分析dump文件

dotnet-dump analyze core_20210510_054712
# 分析gen2中的内容,每个命令的参数以及和含义,可以使用help查看
dg gen2

 

 

 从结果来看,有很多string类型的数据在gen2中,以及mysql的一些数据,我们打开看看具体是什么内容

 

 

看输出,有很多一样的内容,我们随便打开一个看看

 

 

可以看到内容就是数据库的返回数据

 

 同样的方法,我们看看哪些string里面都是什么

 

 

有非常多的对象,我们也是随便打开一个看看内容

 

 

看着像是web的打印

 

 

总结

获取dump文件

dotnet-dump collect <pid>

分析dump文件

dotnet-analyze xxxxx

获取gen2或者其他的内存数据

dg gen2 | gen1 | gen1 | genloh

查看内存数据类型

dumpheap -mt xxxxxx

查看内存数据的具体内容

do xxxxxx

通过具体内容,配合开发人员定位代码问题

 

-------------------------------------------

2021-5-11 更新

再说明一下我们这边的运行环境,以及代码中用到的相关服务

我们正式程序运行在AWS基于Fargate的ECS上,容器配置为0.5vCPU, 512MB内存,.net core程序版本为2.1,数据库查询使用sqlsugar,aws服务用到了Dynamodb和SNS

问题就是程序运行大约1天就出现OOM,导致容器重启。

下面是我们排查问题的过程

Round 1:将core从2.1升级到3.1

原因:根据微软的说法,3.0以后优化了core在linux下以及容器中的性能,降低了内存占用,详见下面的连接。

https://devblogs.microsoft.com/dotnet/using-net-and-docker-together-dockercon-2019-update/

说明:2.1升级到3.1还是有很多地方需要修改的,微软这方面做的就不够好,但是3.1升级5.0据说改动不大,这里要感谢我们的开发同学积极配合修改了代码。

结论:内存增长速度为原来的一半。

内存增长速度变慢了,但是仍然在增长,没有解决根本问题。

Round 2:调整GC模式,从默认的Server GC调整为WorkStation GC

原因:WorkStaionGC会使用更少的内存,回收的更频繁,但是性能可能会稍差一下,根据微软的说法,在docker环境中还是推荐使用WorkStaion GC模式,两种GC的对比,以及推荐详见下文:

https://docs.microsoft.com/en-us/aspnet/core/performance/memory?view=aspnetcore-3.1#workstation-gc-vs-server-gc

结论:内存增长速度又降低了一半,但是仍然在增长,还是没有解决根本问题

Round 3:Gen2中的内存到底是什么?

原因:既然内存不停的在涨,而且通过分析工具可以看到主要是GC中的Gen2部分在增长,按照微软的说法内存中的垃圾数据根据时间的长短,依次存入Gen0, Gen1, Gen2。而且Gen1和Gen2是由core进行垃圾回收的,不需要我们干预,那么Gen2中的内容到底是什么?为什么一直没有被回收?

这一部分就是上面的文章内容,通过以上方法我们已经知道Gen2中的内存主要是变量、数据库查询结果、console控制台打印。奇了怪了,为什么这些东西会在内存里不释放?

关于core的内存管理以及GC原理,见下面的文章

https://docs.microsoft.com/en-us/aspnet/core/performance/memory?view=aspnetcore-3.1

结论:找到了内存中的数据,但是不解这些数据为什么没有被回收

Round 4:关闭这些打印看看

原因:既然Gen2中存在大量的控制台打印,那么我如果关闭控制台打印呢?是不是就没有这部分的内存占用了

结论:没啥作用,内存仍然在不停的增长

Round 5:是不是数据库工具有问题?

原因:既然内存中有大量的数据库查询结果,那么是不是因为我们用了sqlsugar导致的?sqlsugar本身有什么缓存的机制?

我们查了sqlsugar的官方,sqlsugar确实支持二级缓存,但是我们没有用上,详见官方文档:

https://www.donet5.com/home/Doc?typeId=1214

为此,我们直接删除了sqlsugar部分代码,不查询数据库了,直接写死在代码里返回。然后开始跑压力测试(这时候接口已经没有业务逻辑了)

结论:没啥用,内存还在增长

Round 6:放大招了,写一个空接口,没有任何逻辑,直接返回固定字符串

原因:做减法不行,我们开始做加法,从0开始写接口,一点点功能添加,看看到底添加到哪一步的时候,导致内存增加

结论:老实了,内存终于不增长了(准确的说增长的非常缓慢,一晚上增加了0.2%的内存)

至少说明core本身在docker环境下运行,没有明显的内存泄露问题,问题应该出在代码逻辑上

Round 7:使用环境变量限制core的内存使用

原因:开发修改代码去了,我趁机再尝试一次,根据微软的说法,core也是会尽可能多的使用系统内存,以提高性能:

默认情况下,当物理内存负载达到 90%时,垃圾回收对于执行完整的压缩垃圾回收变得更加积极,以避免分页。 当内存负载低于 90% 时,GC 优先使用后台回收进行完整的垃圾回收,这种方法的暂停时间较短,但不会使堆的总大小减少太多。

文档见:https://docs.microsoft.com/zh-cn/dotnet/core/run-time-config/garbage-collector#high-memory-percent

所以,我给docker添加了两个环境变量,理论上只有一个会生效。环境变量的值为16进制。我设置限制内存使用为50%

COMPlus_GCHighMemPercent   0x32
COMPlus_GCHeapHardLimitPercent  0x32

运行结果:

 

红框部分为添加了环境变量的曲线,对比其他曲线,没有变化。

结论:没啥用

 

###############################

今天到此为止,等我们后续的尝试出了结果再更新,希望我们的测试过程对大家有些参考。

------------------------------------------------

 2021-05-13 更新

经过几轮从0开始增加代码的测试,终于定位到问题了:在请求的header校验中,静态变量不释放导致!

解决办法:每次接口校验header时,设置一遍缓存给局部变量,不要每次都new新的缓存。

 

 

posted @ 2021-05-10 16:40  郑立赛  阅读(8576)  评论(21编辑  收藏  举报