一:背景

1. 讲故事

七月中旬有一位朋友加wx求助,他的程序在生产上跑着跑着内存就飙起来了,貌似没有回头的趋势,询问如何解决,截图如下:

和这位朋友聊下来,感觉像是自己在小县城当了个小老板,规律的生活,有当地资源,各种小关系,有一股财务自由的味道,这也是我一直向往的生活方式 。

既然朋友找到我了,我得想办法给他解决问题,既然是内存暴涨,我就赌一把在托管层面吧,嘿嘿,上windbg说话。

二: windbg 分析

1. 托管还是非托管

一直在追这个系列的朋友应该知道,我无数次的用 !address -summary!eeheap -gc 这两个命令来判断当前内存属于托管层还是非托管层。


0:000> !address -summary --- State Summary ---------------- RgnCount ----------- Total Size -------- %ofBusy %ofTotal
MEM_FREE 393 7dfe`f2105000 ( 125.996 TB) 98.43%
MEM_RESERVE 1691 200`0f1e4000 ( 2.000 TB) 99.81% 1.56%
MEM_COMMIT 6191 0`fed07000 ( 3.981 GB) 0.19% 0.00% 0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x000001D2E572BBC8
generation 1 starts at 0x000001D2E54F70E0
generation 2 starts at 0x000001D252051000
ephemeral segment allocation context: none
segment begin allocated size
000001D252050000 000001D252051000 000001D26204FFE0 0xfffefe0(268431328)
Large object heap starts at 0x000001D262051000
segment begin allocated size
000001D262050000 000001D262051000 000001D2655F3F80 0x35a2f80(56242048)
Total Size: Size: 0xbf4dbf80 (3209543552) bytes.
------------------------------
GC Heap Size: Size: 0xbf4dbf80 (3209543552) bytes.

卦象上进程指标为 3.98G ,GC堆指标为 3209543552 = 3G ,很显然,本次事故属于 托管层面

2. 寻找托管层上的大对象

我们都知道C#是托管语言,所以甭管有用没用的对象都逃不出GC堆,言外之意就是看GC堆准没错,挑几个大对象看看。


0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007ff98a68f090 391475 43869284 System.Int32[]
00007ff98b6adfa0 1902760 45666240 System.Collections.ObjectModel.ReadOnlyCollection`1[[System.Linq.Expressions.Expression, System.Linq.Expressions]]
00007ff98b6ac3c0 1951470 46835280 System.Linq.Expressions.ConstantExpression
00007ff98bc452e0 1681178 53797696 System.Linq.Expressions.TypedConstantExpression
00007ff98eacb6b8 1902708 60886656 System.Dynamic.Utils.ListArgumentProvider
00007ff98f236518 1774982 70999280 Microsoft.EntityFrameworkCore.Query.Expressions.ColumnExpression
00007ff98c650c58 1681142 80694816 System.Linq.Expressions.MethodCallExpression3
00007ff98a82bc38 3414094 81938256 System.RuntimeMethodHandle
00007ff98fd96fc0 17750 83936016 System.Collections.Generic.Dictionary`2+Entry[[System.Reflection.MemberInfo, System.Private.CoreLib],[System.Linq.Expressions.Expression, System.Linq.Expressions]][]
00007ff98e5ed5d8 35493 101740504 System.Collections.Generic.Dictionary`2+Entry[[Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceCacheKey, Microsoft.Extensions.DependencyInjection],[System.Object, System.Private.CoreLib]][]
00007ff98bcff6a8 3639389 116460448 System.Linq.Expressions.PropertyExpression
00007ff98b85cf00 5028347 160907104 System.Reflection.Emit.GenericFieldInfo
00007ff98a671e18 2178117 168395994 System.String
00007ff98a5b6610 160565 171498416 System.Object[]
00007ff98eaa8ab0 4981589 199263560 System.Linq.Expressions.MemberAssignment
00007ff98a672360 398740 391928469 System.Byte[]
00007ff98a746d68 181886 486150592 System.Char[]

从托管堆上看,System.Linq.Expressions.MemberAssignment 对象高达 498w ,很明显有问题,从类名看可能和 ExpressionTree 有关,那就抽几个对象看看它的引用链上是否有过大的对象。


0:000> !gcroot 000001d25399f690
HandleTable:
000001D251B715A8 (pinned handle)
-> 000001D262068CF0 System.Object[]
-> 000001D2531C3B78 Microsoft.EntityFrameworkCore.Internal.ServiceProviderCache
-> 000001D25399E3D0 Remotion.Linq.QueryModel
-> 000001D25399E3B8 Remotion.Linq.Clauses.SelectClause
-> 000001D25442C068 System.Linq.Expressions.MemberInitExpression
-> 000001D25442C050 System.Runtime.CompilerServices.TrueReadOnlyCollection`1[[System.Linq.Expressions.MemberBinding, System.Linq.Expressions]]
-> 000001D2539A0290 System.Linq.Expressions.MemberBinding[]
-> 000001D25399F690 System.Linq.Expressions.MemberAssignment

引用链特别长,这里我就截取一下,经过一顿排查,我发现大对象居然是 Remotion.Linq.Clauses.SelectClause,objsize 这个对象直接爆掉了,真的很奇葩,如下代码所示:


0:000> !objsize 000001D25399E3B8
sizeof(000001D25399E3B8) = -1187378032 (0xb93a0c90) bytes (Remotion.Linq.Clauses.SelectClause)

有点懵,这个对象居然是罪魁祸首,从引用链看它是 EF 下的一个构建表达式树的小部件,可以肯定的是,朋友在用EF的时候出了什么问题,不过还得硬着头皮继续挖 SelectClause,经过深挖,我发现这个类有大量这样的 char[] 数组,导出来后大概是下面这样。


Logistics.Text30),
| Text31 = string TryReadValue(t1.Outer.Outer, 42, WmsOutboundConfirmLogistics.Text31),
| Text32 = string TryReadValue(t1.Outer.Outer, 43, WmsOutboundConfirmLogistics.Text32),
| Text33 = string TryReadValue(t1.Outer.Outer, 44, WmsOutboundConfirmLogistics.Text33),
| Text34 = string TryReadValue(t1.Outer.Outer, 45, WmsOutboundConfirmLogistics.Text34),
| Text35 = string TryReadValue(t1.Outer.Outer, 46, WmsOutboundConfirmLogistics.Text35),
| IsQueue = Nullable<bool> TryReadValue(t1.Outer.Outer, 47, WmsOutboundConfirmLogistics.IsQueue),
| IsStop = Nullable<bool> TryReadValue(t1.Outer.Outer, 48, WmsOutboundConfirmLogistics.IsStop),
| CheckCode = string TryReadValue(t1.Outer.Outer, 49, WmsOutboundConfirmLogistics.CheckCode),
| ClientCode = string TryReadValue(t1.Outer.Inner, 50, WmsOutboundOrder.ClientCode),
| WarehouseCode = string TryReadValue(t1.Outer.Inner, 51, WmsOutboundOrder.WarehouseCode),
| ErpNumber = string TryReadValue(t1.Outer.Inner, 52, WmsOutboundOrder.ErpNumber),
| OrderCategory = string TryReadValue(t1.Outer.Inner, 53, WmsOutboundOrder.OrderCategory),
| OrderStatus = string TryReadValue(t1.Outer.Inner, 54, WmsOutboundOrder.OrderStatus),
| OrderType = string TryReadValue(t1.Outer.Inner, 55, WmsOutboundOrder.OrderType),
| SendCompany = string TryReadValue(t1.Outer.Inner, 56, WmsOutboundOrder.SendCompany),
| SendName = string TryReadValue(t1.Outer.Inner, 57, WmsOutboundOrder.SendName),
| SendTel = string TryReadValue(t1.Outer.Inner, 58, WmsOutboundOrder.SendTel),
| SendMobile = string TryReadValue(t1.Outer.Inner, 59, WmsOutboundOrder.SendMobile),
| SendProvince = string TryReadValue(t1.Outer.Inner, 60, WmsOutboundOrder.SendProvince),
| SendCity = string TryReadValue(t1.Outer.Inner, 61, WmsOutboundOrder.SendCity),
| SendArea = string TryReadValue(t1.Outer.Inner, 62, WmsOutboundOrder.SendArea),
| ...
| CategoryName = string TryReadValue(t1.Outer.Inner, 88, WmsOutboundOrder.CategoryName),
| SourcePlatformCode = string TryReadValue(t1.Outer.Inner, 89, WmsOutboundOrder.SourcePlatformCode),
| PayMode = (string)string TryReadValue(t1.Outer.Outer, 90, null),
| List = List<WmsOutboundConfirmLogisticsLinesDTO> WmsOutboundConfirmLogisticsBusiness.GetOrderLines(string TryReadValue(t1.Outer.Outer, 5, WmsOutboundConfirmLogistics.OrderNumber)),
| ConfirmTime = DateTime TryReadValue(t1.Inner, 91, WmsOutboundOrderConfirmation.CreateTime),
| ReturnUrl = (string)string TryReadValue(t1.Outer.Outer, 92, null)
| }
|__ ),
|__ contextType: Core.DataRepository.BaseDbContext,
|__ logger: DiagnosticsLogger<Query>,
|__ queryContext: Unhandled parameter: queryContext)

从内容看,应该是 select 语句的ExpressionTree表示,问了下朋友,说大概是报表业务,不过这些信息给他貌似也没有多大帮助,说实话到这里我其实也不知道怎么继续往下排查了,陷入了绝望。

3. 从绝望中寻找希望

我在想,既然EF构建了大量这样的 ExpressionTree,肯定有问题,但也想不出是什么问题,隔了半天,我突然灵光一现,EF既然构建了树,有可能sql也出来了,对,我何不直接在 heap 上搜索 select 的sql语句。。。。


0:000> !strings /m:*select*
Address Gen Length Value
000001d2e4de64e0 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4e11e78 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4e3d1f0 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4e673c8 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4e91760 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4ebb2e8 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4ee54f8 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4f10758 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",...
000001d2e4f398d0 2 1964 SELECT a."Id", a."CreateTime" AS "CreateTime0", a."CreatorId", a."CreatorRealName", a."Deleted", a."OrderNumber", a."CarrierId",... ---------------------------------------
18128 matching strings

果然发现了大量重复的 select 语句,而且从最左边的内存地址看都是非常接近的,也就说明他们是在某一个操作中同时生成的,然后我们导出几个sql语句。


SELECT a."Id", ....
FROM "WmsOutboundConfirmLogistics" AS a
INNER JOIN "WmsOutboundOrder" AS b ON a."OrderNumber" = b."OrderNumber"
INNER JOIN "WmsOutboundOrderConfirmation" AS c ON a."OrderNumber" = c."OrderNumber"
WHERE (a."OrderNumber" = @__pagination_OrderNumber_0) AND (b."FreezeStatus" = FALSE)
ORDER BY a."Id""; SELECT a."Id", ....
FROM "WmsOutboundConfirmLogistics" AS a
INNER JOIN "WmsOutboundOrder" AS b ON a."OrderNumber" = b."OrderNumber"
INNER JOIN "WmsOutboundOrderConfirmation" AS c ON a."OrderNumber" = c."OrderNumber"
WHERE (a."OrderNumber" = @__pagination_OrderNumber_0) AND (b."FreezeStatus" = FALSE)
ORDER BY a."Id""

拿到这 1.8w 重复的sql 给朋友看,朋友说这是查询报表的sql。

4. 所有线索整合打通

那这里就存在着很大问题,既然是查询报表,为什么会有 1.8w 相同的sql,唯一不同的就是 a."OrderNumber" = @__pagination_OrderNumber_0 中的订单号,难道不应该是 a.OrderNumber in (xxxx) 或者是表关联查询吗??? 整理一下就是下面这样的猜想:


-- 理想
select * from a where a.id in (1,2,3) -- 现实
select * from a where a.id=1;
select * from a where a.id=2;
select * from a where a.id=3;

加上每个sql内存地址相近,再结合爆表的 Remotion.Linq.Clauses.SelectClause 对象,整个流程大概就是:本该表关联或者in操作,结果变成了无数个单条sql语句查询,导致EF底层出现内存爆炸式增长。

三:总结

看了下朋友查询ef的写法,猜测大多都是人肉构建 ExpressionTree 去查询数据库,大写的,比如下面的这张图:

解决方案就是让朋友检查下表示式树的写法问题,或者直接灌写好的sql得了,说实话这个dump还是费了九牛二虎之力,本以为很简单,实操起来还是碰到了一点小困难,就当历练成长吧!

更多高质量干货:参见我的 GitHub: dotnetfly

记一次 .NET 某WMS仓储打单系统 内存暴涨分析的更多相关文章

  1. 记一次 .NET 某三甲医院HIS系统 内存暴涨分析

    一:背景 1. 讲故事 前几天有位朋友加wx说他的程序遭遇了内存暴涨,求助如何分析? 和这位朋友聊下来,这个dump也是取自一个HIS系统,如朋友所说我这真的是和医院杠上了,这样也好,给自己攒点资源, ...

  2. 记一次 .NET医疗布草API程序 内存暴涨分析

    一:背景 1. 讲故事 我在年前写过一篇关于CPU爆高的分析文章 再记一次 应用服务器 CPU 暴高事故分析 ,当时是给同济做项目升级,看过那篇文章的朋友应该知道,最后的结论是运维人员错误的将 IIS ...

  3. 记一次 .NET 某招聘网后端服务 内存暴涨分析

    一:背景 1. 讲故事 前段时间有位朋友wx找到我,说他的程序存在内存阶段性暴涨,寻求如何解决,和朋友沟通下来,他的内存平时大概是5G 左右,在某些时点附近会暴涨到 10G+, 画个图大概就是这样. ...

  4. 记一次 .NET 医院CIS系统 内存溢出分析

    一:背景 1. 讲故事 前几天有位朋友加wx求助说他的程序最近总是出现内存溢出,很崩溃,如下图: 和这位朋友聊下来,发现他也是搞医疗的,哈哈,.NET 在医疗方面还是很有市场的,不过对于内存方面出的问 ...

  5. 记一次 .NET 某智能服装智造系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友找到我,说他的程序出现了内存泄漏,不知道如何进一步分析,截图如下: 朋友这段话已经说的非常言简意赅了,那就上 windbg 说话吧. 二:Windbg 分析 1. ...

  6. 记一次 WinDbg 分析 .NET 某工厂MES系统 内存泄漏分析

    一:背景 1. 讲故事 上个月有位朋友加微信求助,说他的程序跑着跑着就内存爆掉了,寻求如何解决,截图如下: 从聊天内容看,这位朋友压力还是蛮大的,话说这貌似是我分析的第三个 MES 系统了,看样子 . ...

  7. 记一次 .NET 某消防物联网 后台服务 内存泄漏分析

    一:背景 1. 讲故事 去年十月份有位朋友从微信找到我,说他的程序内存要炸掉了...截图如下: 时间有点久,图片都被清理了,不过有点讽刺的是,自己的程序本身就是做监控的,结果自己出了问题,太尴尬了 二 ...

  8. 记一次 .NET 某电厂Web系统 内存泄漏分析

    一:背景 1. 讲故事 前段时间有位朋友找到我,说他的程序内存占用比较大,寻求如何解决,截图就不发了,分析下来我感觉除了程序本身的问题之外,.NET5 在内存管理方面做的也不够好,所以有必要给大家分享 ...

  9. 记一次 .NET 某智慧物流 WCS系统 CPU 爆高分析

    一:背景 1. 讲故事 哈哈,再次见到物流类软件,上个月有位朋友找到我,说他的程序出现了 CPU 爆高,让我帮忙看下什么原因,由于那段时间在苦心研究 C++,分析和经验分享也就懈怠了,今天就给大家安排 ...

随机推荐

  1. 图的存储与遍历C++实现

    1.图的存储 设点数为n,边数为m 1.1.二维数组 方法:使用一个二维数组 adj 来存边,其中 adj[u][v] 为 1 表示存在 u到 v的边,为 0 表示不存在.如果是带边权的图,可以在 a ...

  2. hdu 6050 Funny Function 矩阵快速幂

    就算告诉我是矩阵快速幂我也推不出递推式呀!!! 官方题解: 对于任意i>=1,当j>=3时,有通过归纳法可以得到 进而推导出 后来自己重新推导了一遍 #include <iostre ...

  3. CRM的职能和主要构成模块探索

    CRM客户管理系统是随着互联网的快速发展和信息技术的进步而发展起来的,是企业管理客户关系.优化业务流程的首选.客户关系管理可以存储企业获得的客户信息,方便业务人员随时查看和了解客户需求:客户关系管理可 ...

  4. Python迭代器和生成器你学会了吗?

    在了解什么是迭代器和生成器之前,我们先来了解一下容器的概念.对于一切皆对象来说,容器就是对象的集合.例如列表.元祖.字典等等都是容器.对于容器,你可以很直观地想象成多个元素在一起的单元:而不同容器的区 ...

  5. css实现相框效果

    <!DOCTYPE html><html> <head> <meta charset="utf-8"> <title>& ...

  6. Java:Java的^运算符详解

    按位异或运算符(^)是二元运算符,要化为二进制才能进行计算,在两个操作数中,如果两个相应的位相同,则运算结果为0,否则1:例如:int a=15:int b=a^8;运算结果为:a=15:b=7:a( ...

  7. 在Java中如何高效判断数组中是否包含某个元素

    如何检查一个数组(无序)是否包含一个特定的值?这是一个在Java中经常用到的并且非常有用的操作.同时,这个问题在Stack Overflow中也是一个非常热门的问题.在投票比较高的几个答案中给出了几种 ...

  8. 什么是BSE

    BSE (bridge system engineer) 是外包开发人员和客户之前的桥梁. 主要是将客户的需求准确的理解并传达给外包的开发人员,一般情况下也兼开发的 leader 工作. 参考: ht ...

  9. Twain Capabilities

    Paper Handling 纸操作 CAP_AUTOFEED MSG_SET为TRUE,启用Twain源的自动进纸. CAP_CLEARPAGE MSG_SET为TRUE,退出当前页面并清空数据. ...

  10. 微信小程序云开发-云存储的应用-云相册

    一.准备工作 1.创建数据库表images 2.设置数据库表images的权限 二.创建图片列表页 创建图片列表页imageList,用于展示图片列表.该页面具有跳转到图片上传页面.图片列表展示.删除 ...