• 【.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新的缓存。

    原创作者:郑立赛


    邮箱:zhenglisai@qq.com


    欢迎关注我们的公众号获取最新文章:运维自动化开发


    公众号
    公众号
  • 相关阅读:
    017 文件xfs_repair恢复,xfs_dump恢复,lvm动态扩容
    003 rsync客户端与服务端小脚本
    002 rsync守护进程传输方式详解
    001 期中架构简介、备份初识
    016 netstat、磁盘分区(fdisk、gdisk)
    015 Linux中常用的信号、HUP信号
    014 进程(PS与TOP)
    013 源码安装(Nginx&php为例)
    本地、远程仓库的搭建
    第八章
  • 原文地址:https://www.cnblogs.com/zhenglisai/p/14751677.html
Copyright © 2020-2023  润新知