记一次 .NET 某WMS仓储打单系统 内存暴涨分析

一:背景

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: nonesegment             begin         allocated              size
000001D252050000  000001D252051000  000001D26204FFE0  0xfffefe0(268431328)
Large object heap starts at 0x000001D262051000segment             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还是费了九牛二虎之力,本以为很简单,实操起来还是碰到了一点小困难,就当历练成长吧!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/299576.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一分钟读懂一个数学时代,看完不跪算我输!

▲ 点击查看上帝说&#xff0c;要有光&#xff0c;于是便有了光。而香农说&#xff0c;要有熵&#xff0c;于是信息化时代正式拉开帷幕。克劳德艾尔伍德香农&#xff08;Claude Elwood Shannon&#xff09;被尊称为“信息论之父”。不管你是否知道他&#xff0c;是如何看待他…

秋招面试我去了拼多多,直接被问JVMGC底层原理和算法,我吊打面试官

JVM 常用参数设置积累 # 堆的初始值&#xff0c;默认物理内存的1/64 -Xms: # 堆的最大值&#xff0c;默认物理内存的1/4 -Xmx: # 年轻代大小「在整个堆内存大小确定的情况下&#xff0c;增大年轻代将会减小年老代&#xff0c;反之亦然。此值关系到JVM垃圾回收&#xff0c;对系…

php中使用exec,system等函数调用系统命令

2019独角兽企业重金招聘Python工程师标准>>> 注意:要想使用这二个函数php.ini中的安全模式必须关闭&#xff0c;要不然为了安全起见php是不让调用系统命令的。  先看一下php手册对这二个函数的解释:  exec --- 执行外部程式  语法 : string exec ( string com…

了解jQuery技巧来提高你的代码

jQuery之所以如此流行并被从大公司到个人博客的几乎每个人都广泛使用&#xff0c;是因为它上手和使用相当简单&#xff0c;而且为我们提供了一些人都不知道的相当棒的特性。我认为jQuery的大多数用户更趋向于使用jQuery插件来解决面临的难题&#xff0c;这通常是明智的选择。但…

如何主动清空.NET数据库连接池?

一般我们的项目中会使用1到2个数据库连接配置&#xff0c;同程艺龙的数据库连接配置被收拢到统一的配置中心&#xff0c;由DBA统一维护&#xff0c;业务方通过某个配置字符串拿到的是开箱即用的Connection对象。DBA能在对业务方无侵入的情况下&#xff0c;给业务方切换备份数据…

假如有人在今天炸了支付宝的存储服务器...

全世界只有3.14 % 的人关注了青少年数学之旅今天在知乎看到了一个问题《假如有人把支付宝存储服务器炸了&#xff08;物理炸&#xff09;&#xff0c;大众在支付宝里的钱是不是就都没有了呢&#xff1f;》外行人问题。网站都是有服务器的&#xff0c;服务器都是有实体的。那么支…

Cookie全解

1. Cookie 可以存储哪些值 在 Cookie 中只能存储个人可识别信息. 个人可识别信息是指可以用来识别或联系用户的信息. 例如用户的姓名, 电子邮件, 家庭住址等. 必须强调的是, 这些可识别信息必须是非机密或重要信息. 2. 使用 Cookie 对象保存和读取客户端信息. 要存储一个 Cooki…

代码格式

2019独角兽企业重金招聘Python工程师标准>>> 1.参考&#xff1a;JavaScript程序编码规范 转载于:https://my.oschina.net/u/1791074/blog/283578

94年出生,6篇SCI,一作发Science,你还不放下手上玩的泥巴

全世界只有3.14 % 的人关注了 青少年数学之旅 2019年9月27日&#xff0c;国际顶尖期刊《科学》&#xff08;Science&#xff09;杂志在线以全文Article的形式发表了北京航空航天大学材料科学与工程学院赵立东教授课题组在热电材料研究上取得的新进展&#xff0c;北京航空航天大…

一个问题让我直接闭门思过!!!拼多多面试必问项之List实现类:LinkedList

一、LinkedList概述 1、对于频繁的插入或删除元素的操作&#xff0c;建议使用LinkedList类&#xff0c;效率较高。 2、LinkedList是一个实现了List接口和Deque接口的双端链表。 3、LinkedList底层的链表结构使它支持高效的插入和删除操作&#xff0c;另外它实现了Deque接口&a…

Docker小白到实战之开篇概述

前言“不对啊&#xff0c;在我这运行很正常啊”&#xff0c;这句话小伙伴们在前几年应该听得很多&#xff1b;每次一到安装、部署时总有一堆问题&#xff0c;毕竟操作系统版本、软件环境、硬件资源、网络等因素在作怪&#xff0c;此时难免会导致开发小伙伴和运维哥们互相甩锅&a…

设置su为不需要密码切换为root

设置su为不需要密码 如果需要对某用户su命令也不需要输入密码&#xff0c;则需要修改下列的&#xff1a;1--->如果没有wheel组 则用sudo groupadd wheel创建命令为 sudo groupadd wheel&#xff1b;2---->sudo vim /etc/group将username和root加入到wheel用户组内 如图&a…

被女朋友拉黑后,我写了个“舔狗”必备神器

全世界只有3.14 % 的人关注了 青少年数学之旅 “ 在一个阳光明媚的清晨&#xff0c;我打开窗户呼吸了一口新鲜空气。阳光灿烂&#xff0c;岁月静好&#xff0c;又是一个约女朋友出去爬山吃饭看电影的好日子。 图片来自包图网 想到女朋友的大眼睛&#xff0c;我脸上不禁洋溢起了…

涨薪关键之反射机制,引得项目经理对你的看重,加薪触手可及!!!!

前言 就比如我前几天被面试官问什么是反射&#xff1f;&#xff1f;&#xff1f; 而我的回答是&#xff01;&#xff01;&#xff01; 反射是动态语言的关键&#xff0c;反射允许程序在执行期间借助Reflection API取得任何类的内部信息&#xff0c;并能直接操作任曦对象的内…

如何摆脱「自我否定」状态

大家好&#xff0c;我是Z哥。你最近正处于自我否定的状态吗&#xff1f;如果不是的话&#xff0c;回想一下最近的一次处于这种状态是什么时候&#xff1f;当时的感受如何&#xff1f;以及&#xff0c;最终是如何走出这个状态的&#xff1f;不着急&#xff0c;给你 1 分钟回忆一…

struct and union

[url]http://hi.baidu.com/tweigh/blog/item/5303d2ef6e2720eace1b3e9d.html[/url]1. struct的巨大作用面对一个人的大型C/C程序时&#xff0c;只看其对struct的使用情况我们就可以对其编写者的编程经验进行评估。因为一个大型的C/C程序&#xff0c;势必要 涉及一些(甚至大量)进…

编码GBK的不可映射字符

为什么80%的码农都做不了架构师&#xff1f;>>> 由于JDK是国际版的&#xff0c;在编译的时候&#xff0c;如果我们没有用-encoding参数指定我们的JAVA源程序的编码格式&#xff0c; 则javac.exe首先获得我们操作系统默认采用的编码格式&#xff0c; 也即在编译java…

低调的大神!他改变了半导体产业!史上唯一两次获得诺贝尔物理奖,却几乎被人遗忘...

全世界只有3.14 % 的人关注了青少年数学之旅两次获得诺贝尔奖的科学家&#xff0c;世界上仅有这四个人&#xff01;他们是&#xff1a;1. 居里夫人(Marie Curie,1867~1934),波兰科学家,他的丈夫叫皮埃尔居里,两人合称“居里夫妇”! 1903年,居里夫妇和亨利...2.约翰巴丁 美国物理…

入职第一天,我接手了号称【屎山】的祖传代码,这还能卷吗???

公司各种各样的祖传代码都是令新人虎躯一震的代码&#xff0c;因为有时候你根本不知道它是干嘛的&#xff0c;甚至觉得它毫无用处&#xff0c;关键是 还绝对不能动&#xff0c;碰一段改半年&#xff0c;别问我怎么知道的。最讽刺的是&#xff0c;你可能为了修改代码&#xff0c…

设计模式之迭代器

迭代器模式介绍集合的结构迭代器模式是一种行为设计模式&#xff0c;让你能在不暴露集合底层表现形式(列表、栈、树等)的情况下遍历集合中所有的元素。迭代器模式满足了单一职责和开闭原则&#xff0c;外界的调用方也不需要知道任何一个不同的数据结构在使用上的遍历差异。迭代…