前言

这周系统更新了一个版本,部署到线上.

客户反馈整个系统全部都卡顿,随即我们上服务器检查

发现整个服务器内存竟然达到了20-30G的占用..如图:

其中有一个订单服务,独自占用13-18G内存,

当它重启以后,内存会降低下来一段时间,但过不了多久 就又会增长上去

高度怀疑出现了内存溢出的情况,由于是线上服务器而且是离线内网.

项目又都运行在docker容器中,容器为了最小化,采用了极简的系统,几乎任何常见命令都没有.

所以考虑采用挂载额外辅助容器的形式进行调试.

正文

1.创建调试用的辅助容器

这个简单,我们直接创建DockerFile如下:

# 使用 .NET 5 SDK 作为基础镜像
FROM mcr.microsoft.com/dotnet/sdk:5.0 # 安装 dotnet-dump 、 dotnet-stack 、dotnet-counters、dotnet-trace的旧版本(兼容 .NET 5)
RUN dotnet tool install -g dotnet-dump --version 5.0.220101 \
&& dotnet tool install -g dotnet-stack --version 1.0.130701 \
&& dotnet tool install -g dotnet-counters --version 5.0.251802 \
&& dotnet tool install -g dotnet-trace --version 5.0.251802 \
&& apt-get update \
&& apt-get install -y unzip procps \
&& echo 'export PATH="$PATH:/root/.dotnet/tools"' >> /root/.bashrc #设定全局工具环境变量
ENV PATH="${PATH}:/root/.dotnet/tools" # 默认启动 bash 以方便交互
CMD ["/bin/bash"]

这里我们使用当前系统RunTime对应版本的SDK作为基础镜像.

然后安装我们调试程序信息需要的工具:dotnet-dump 、 dotnet-stack 、dotnet-counters、dotnet-trace

先介绍一下这些工具.

dotnet-dump:可以收集和分析 Windows、Linux 和 macOS dump的信息,可以运行 SOS 命令来分析崩溃和垃圾回收器 (GC)。

dotnet-stack:可以收集 .NET 进程中的所有线程捕获和打印托管堆栈。

dotnet-counters:是一个性能监视工具,可以临时监视.NET程序的运行状况和做一些初级的性能调查

dotnet-trace:在不使用本机探查器的情况下对正在运行的.NET Core 进程进行跟踪

2.将辅助调试容器附加到应用容器运行

首先我们需要重新run一个应用容器,并给它特权模式和系统级的调试权限,大概命令如下:

docker run -d --name myapp --privileged=true --cap-add=SYS_PTRACE -e ASPNETCORE_ENVIRONMENT=dev -e COMPlus_EnableDiagnostics=1 --volume /home/tmp:/tmp order-test:5.0

重点是privileged参数和cap-add参数,还有应用系统的tmp文件夹需要映射到宿主机

然后我们运行我们的调试容器并附加到应用容器

docker run -it --rm  --cap-add=SYS_PTRACE --pid=container:myapp --privileged=true --volume /home/tmp:/tmp dotnet-debug-tools:5.0

同样,它也需要privileged参数和cap-add参数,tmp临时文件也需要映射在宿主机和应用容器同样的目录下

注意--pid=container:myapp 中的myapp 是上面应用容器的名称.

然后,我们在调试容器直接运行命令:

ps aux

应该就能看到应用服务中的dotnet的进程了.因为在容器中运行,所以一般dotnet的PID是1,如图:

3.分析应用容器内dotnet进程的情况.

我们可以先使用dotnet-counters进行监控.

命令如下,其中-p是dotnet的进程编号:

dotnet-counters monitor -p 1

能得到如下结果:

% Time in GC since last GC (%)                                 0
Allocation Rate (B / 2 sec) 98,016
CPU Usage (%) 0
Exception Count (Count / 2 sec) 0
GC Fragmentation (%) 8.189
GC Heap Size (MB) 11,419
Gen 0 GC Count (Count / 2 sec) 0
Gen 0 Size (B) 192
Gen 1 GC Count (Count / 2 sec) 0
Gen 1 Size (B) 36,742,336
Gen 2 GC Count (Count / 2 sec) 0
Gen 2 Size (B) 8.8066e+09
IL Bytes Jitted (B) 7,248,623
LOH Size (B) 3.3414e+09
Monitor Lock Contention Count (Count / 2 sec) 0
Number of Active Timers 209
Number of Assemblies Loaded 426
Number of Methods Jitted 135,063
POH (Pinned Object Heap) Size (B) 1,299,832
ThreadPool Completed Work Item Count (Count / 2 sec) 0
ThreadPool Queue Length 0
ThreadPool Thread Count 12
Working Set (MB) 14,910

我们可以直接借助AI分析这个性能指标如下:

指标 当前值 分析建议
CPU Usage (%) 0 CPU 几乎未被使用,说明当前应用 处于空闲或阻塞状态。如果此时预期它应该在处理请求,说明可能卡在 I/O、锁、数据库等等待资源上。
GC Heap Size (MB) 11,419 MB 堆内存非常大(约 11GB),说明分配对象多或存在内存泄漏风险。建议配合 dump 分析对象分布。
GC Fragmentation (%) 8.189% 碎片率较低,尚可接受。通常低于 20% 问题不大。
LOH Size (B) ~3.34 GB Large Object Heap(LOH)占用了非常大的空间。意味着有很多大对象(如数组、字符串、缓存等)未被及时回收,或持续增长。需要进一步 dump 分析。
Gen 2 Size (B) ~8.8 GB Gen2 表示长时间存活对象。此值非常高,说明内存中存在大量老对象未被释放,极可能存在内存泄漏
Gen 0/1 GC Count 0 说明当前没有发生 GC,或者是刚启动。这种情况下内存持续增长会导致最终触发 GC 或 OOM。
% Time in GC since last GC 0% 目前没有 GC 时间消耗,结合上面 GC Count 是 0,一致。
Working Set (MB) 14,910 MB 进程实际占用物理内存约 14GB,结合堆大小与 LOH/Gen2 数值合理,但这也是很大的内存占用,需要关注增长趋势
Allocation Rate 98 KB / 2 sec 内存分配速率很低(接近空闲),说明当前没有明显的内存增长。
ThreadPool Thread Count 12 线程池线程数正常范围,无需担心。
ThreadPool Queue Length 0 没有待处理任务,说明任务执行速度没有瓶颈。
Exception Count 0 无异常抛出,良好。
Monitor Lock Contention Count 0 没有锁争用,说明线程间竞争不激烈。

根据AI的分析报告,我们可以得知:

GC Heap Size (MB) 堆内存极大达到了11G

LOH Size (B)大对象指标也很大,有3G的大对象

Gen2 长期活动的对象很多,占用了8G

这样我们基本就可以断定是在应用中出现了内存泄漏的情况.

也不用额外在看别的信息了. 我们直接使用dotnet-dump 抓取内存快照进行分析,抓取命令如下:

dotnet-dump collect -p 1

我们会得到如下结果:

Writing minidump with heap to ./core_20250514_030614
Complete

由于内存快照比较大,复制回来分析..难度比较高.我们可以直接继续利用dotnet-dump analyze 在线上分析

执行命令如下:

dotnet-dump analyze core_20250514_030614

然后我们就可以使用sos命令进行分析了.

既然是内存泄漏,我们直接查看托管堆里面的到底是啥情况,命令如下:

dumpheap -stat

正常是按从小到大排序的..所以很尬,我们划到最下面,看到如下结果:

由于我比较清楚这个代码的情况,直接发现了一个很明显的问题,

Order.OperApply.ApplyVoucherDetail  只是一个业务实体而已,但是在堆里面有29W个对象,

明显是很不合理的.

我们直接分析它.可以使用dumpheap -mt 获取它的所有实列地址.

dumpheap -mt 00007f117f67e590

可以得到如下结果:

我们选择最后一个Adderss寻找对象的根方法.命令如下:

gcroot 00007f0ca9b55818

可以看到如下代码内容(由于太多,我只贴出来有用的):

->  00007F0E643FB770 RabbitMQ.Client.Impl.AsyncConsumerWorkService
-> 00007F0DA4D0BBB8 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1+AsyncStateMachineBox`1[[System.Threading.Tasks.VoidTaskResult, System.Private.CoreLib],[StockManage.Handler.ProductInOutStockStatDistributedHandler+<HandleEventAsync>d__6, StockManage.Application]]
-> 00007F0AFCCE9020 System.Collections.Generic.Dictionary`2+Entry[[System.Object, System.Private.CoreLib],[Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry, Microsoft.EntityFrameworkCore]][]
-> 00007F1035A778A0 Order.OperApply.ApplyVoucher

这四条,我们就可以看出来,是RabbitMQ的消息队列,在ProductInOutStockStatDistributedHandler方法,进行消费的时候

会通过EF CORE 创建这个ApplyVoucher的实例,接下来,我们就需要查看这个ProductInOutStockStatDistributedHandler,到底做了什么.

4.排查代码

直接去查看这个方法的代码,发现竟然没有任何一处使用了ApplyVoucher实体.

所以,我们直接运行本地调试,发现在这个方法结束后会去查询ApplyVoucher表.

分析代码后,我们发现,由于我们使用的是ABP的框架,在方法结束后,会自动写入审计日志,最后才会结束整个调用.

遂调查审计日志模块,发现有小伙伴在审计日志中对ABP的AuditLogInfo对象进行了序列化操作.

查询ABP源码发现,AuditLogInfo的EntityChanges中竟然储存了Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry对象

而EntityEntry对象又包含了整个DBContext上下文..然而我们的DBContext 又开启了懒加载的功能

所以当它被序列化的时候...等于在序列化整个数据库..(这里省略一百个C...)

赶紧屏蔽这段代码,并更新到线上. 问题瞬间解决了..

5.深入求证.

解决问题后,还是比较好奇,有没有同样使用ABP的兄弟遇见相关问题,遂去查询abp源码仓库..

竟然发现在Extracting a Module as a Microservice的相关说明里.

看到了这一段...

而且ABP框架还特意创建了一个IAuditLogInfoToAuditLogConverter来转换AuditLogInfo对象..方便后面进行序列化和存储..

后记

这一次分析线上问题的过程,还是比较有参考性的,所以记录一下.也希望对各位兄弟们有帮助.觉得OK的可以点个推荐~~.3Q~

记一次ASP.NET CORE线上内存溢出问题与dotnet-dump的排查方法的更多相关文章

  1. 记一次asp.net core 线上崩溃解决总结

    1.首先要先准备好环境,安装lldb 工具 要安装3.9版本的,因为每个版本对应dnc版本不一样,3.9的支持2.2 版本,然后确定分析的机器里dnc 版本和线上的生产环境是否一致,自己安装比较费劲, ...

  2. ASP.NET Core文件上传IFormFile于Request.Body的羁绊

    前言 在上篇文章深入探究ASP.NET Core读取Request.Body的正确方式中我们探讨了很多人在日常开发中经常遇到的也是最基础的问题,那就是关于Request.Body的读取方式问题,看是简 ...

  3. C# 6 与 .NET Core 1.0 高级编程 - 40 ASP.NET Core(上)

    译文,个人原创,转载请注明出处(C# 6 与 .NET Core 1.0 高级编程 - 40 章  ASP.NET Core(上)),不对的地方欢迎指出与交流. 章节出自<Professiona ...

  4. [转载]ASP.NET Core文件上传与下载(多种上传方式)

    ASP.NET Core文件上传与下载(多种上传方式)   前言 前段时间项目上线,实在太忙,最近终于开始可以研究研究ASP.NET Core了. 打算写个系列,但是还没想好目录,今天先来一篇,后面在 ...

  5. 放码来战!HMS Core线上Codelabs挑战赛正式开始

    亲爱的开发者,在1024程序员节即将到来之际,HMS Core准备了一场线上Codelabs挑战赛,现向你发出诚挚邀请,希望你能将新奇的想法和对产品的思考融入代码,用技术与世界对话. HMS Core ...

  6. 码上来战!探索“智”感生活,HMS Core线上Codelabs挑战赛第4期开始!

    HMS Core线上Codelabs挑战赛第4期正式开始!我们向所有实践力超强.创新力满满的开发者发出邀请,用你的超级"码"力,解锁更多应用价值! 生活里,我们被手机"秒 ...

  7. ASP.NET Core - 缓存之内存缓存(上)

    1. 缓存 缓存指的是在软件应用运行过程中,将一些数据生成副本直接进行存取,而不是从原始源(数据库,业务逻辑计算等)读取数据,减少生成内容所需的工作,从而显著提高应用的性能和可伸缩性,使用好缓存技术, ...

  8. ASP.NET Core - 缓存之内存缓存(下)

    话接上篇 [ASP.NET Core - 缓存之内存缓存(上)],所以这里的目录从 2.4 开始. 2.4 MemoryCacheEntryOptions MemoryCacheEntryOption ...

  9. 记一次asp.net core 在iis上运行抛出502.5错误

    asp.net core 在iis上运行抛出502.5异常的部分原因以及解决方案 环境说明 已安装 .net core runtime 2.1.401 已安装 .net core windows ho ...

  10. ASP.NET Core 文件上传

    前言 上篇博文介绍了怎么样在 asp.net core 使用 Redis 和 Protobuf 进行 Session缓存.本篇的是开发过程中使用的一个小功能,怎么做单文件和多文件上传. 如果你觉得对你 ...

随机推荐

  1. 使用Go复刻skiplist核心功能

    0.引言 正好做LC每日一题要求实现一个跳表,于是学习了redis的扩展skiplist,并使用Go进行复刻学习.学习参考了文章:Redis内部数据结构详解(6)--skiplist - 铁蕾的个人博 ...

  2. 【答题系统可参考】php 禁止api被跨域调用

    在 PHP 中,防止 API 被跨域调用可以通过设置适当的 HTTP 响应头来实现.跨域资源共享(CORS,Cross-Origin Resource Sharing)机制允许或拒绝来自不同源的请求. ...

  3. AI与.NET系列文章之三:在.NET中使用大语言模型(LLMs)

    引言 在技术迅猛发展的今天,大语言模型(Large Language Models, LLMs)已成为人工智能领域的核心驱动力之一.从智能对话系统到自动化内容生成,LLMs的应用正在深刻改变我们的工作 ...

  4. 【ABAQUS 二次开发笔记】使用keyword 、python和matlab一起处理Odb数据

    用conversion shell element (S4R单元)建模层合板,有6层ply,每个lamina(ply)有3个 integration point,共计18个integration po ...

  5. dig命令命令常见用法

    域名结构 主机名.次级域名.域名.根域名 host. sld .tld .root 几种常见的解析类型 A记录 CNAME txt记录 dig命令命令常见用法 dig,"domain inf ...

  6. 【P0】Logisim部件级实验/有限状态机

    课上 过得十分狼狈.经鉴定孩子可能脑子拗 T1 投票决议 组内投票,赞成>反对,则通过:组长拥有一票否决权. 信号名 方向 描述 [1:0] s Input 2'b00 赞成2'b01 反对2' ...

  7. Manus的开源复刻OpenManus初探

    OpenManus介绍 Manus需要邀请码才能体验,目前大部分人都体验不到. 有几个大佬花3个小时就复现了一个简单的原型OpenManus,让我们体验体验吧!! 截至目前,该项目已经获得了25.9k ...

  8. Mqtt集成与设计

    Mqtt集成 集成mqtt,我们首先得明白什么是mqtt: MQTT(Message Queuing Telemetry Transport)是一种轻量级.基于发布-订阅模式的消息传输协议,适用于资源 ...

  9. 一文速通Python并行计算:04 Python多线程编程-多线程同步(上)—基于条件变量、事件和屏障

    一文速通 Python 并行计算:04 Python 多线程编程-多线程同步(下)-基于条件变量.事件和屏障 摘要: 本文介绍了 Python 多线程同步的三种机制:条件变量(Condition).事 ...

  10. 【Linux】5.3 Shell字符串

    Shell 字符串 字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号.单双引号的区别跟PHP类似. 1. 单 ...