现象
随着程序运行,内存占用率越来越高,直到触发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
分析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新的缓存。