天天看点

Windbg分析高内存占用问题

最近产品发布大版本补丁更新,一商超客户升级后,反馈系统经常奔溃,导致超市的收银系统无法正常收银,现场排队付款的顾客更是抱怨声声。为了缓解现场的情况, 客户都是手动回收IIS应用程序池才能解决。

这样的后果是很严重的,接到反馈,第一时间想到的是加内存吧,这样最快。但是客户从8G-->16G-->32G,只是延长了每次奔溃的时间,但是并没有解决系统卡顿的问题。到这里,也基本猜测了问题所在了,肯定是什么东西一直在吃内存且得不到释放。这种问题,也就只能打Dump分析了。

远程客户应用服务器,32G内存占用已经消耗了78%,而现场已经反馈收银系统接近奔溃了,要求先强制回收内存。反正也要奔溃了,先打Dump再说吧。

(PS:打Dump会挂起进程,导致应用无法响应!而打Dump的耗时,也是根据当时进程的内存占用有关,内存占用越大,耗时越久。)

打开任务管理器,选择对应的IIS进程,右键创建转储文件(Dump)。

结果,Dump文件是生成的,结果当分析的时候,发现Windbg提示Dump无效。说明Dump文件创建的有问题。观察任务管理器,发现内存占用一下就降下来了,原来是之前的进程直接奔溃了,重启了一个W3WP进程。

既然直接从任务管理器无法创建,就使用第三方工具收集Dump吧。经过Goggle,找到一款很好用的Dump收集工具ProcDump,是一个命令行应用,其主要用途是监视应用程序的CPU或内存峰值并在峰值期间生成Dump。

因为是高内存占用问题,我们使用以下命令来抓取dump:

(PS:可以使用进程名称,也可以使用进程ID来指定要创建Dump的进程。当有多个相同名称的进程时,必须使用进程ID来指定!)

procdump w3wp -m 20480 -o D:\Dumps (当内存超过20G时抓取一个w3wp进程的MiniDump)

上面就是我踩得第一个坑,因为默认抓取的是MiniDump,很快就抓下来,文件也很小,正在我得意的时候,Windbg加载Dump分析的时候,发现包含的信息很少,根本无法进行进一步的分析。

调整创建Dump的命令,添加<code>-ma</code>参数即可创建完整Dump。

procdump w3wp -ma -m 20480 -o D:\Dumps (当内存超过20G时抓取一个w3wp进程的完整Dump)

结果再一次,当内存占用到达20G,占比80%的时候,Dump再次创建失败,提示:<code>Procdump Error writing dump file</code>。再一次感觉到绝望。不过至少有错误提示,Google一把,果然存在天涯沦落人。Procdump Error writing dump file: 0x80070005 Error 0x80070005 (-2147024891): Access is denied。大致的意思是说,当90S内Dump文件没有成功创建的话(也就意外这w3wp进程被挂起了90s),IIS检测到w3wp进程挂起超过90s没有响应就会终止进程,重现创建一个新的进程。好嘛,真是处处是坑。

这个坑,也让我开始真正停下来思考问题。罗马不是一日建成的,内存也不是一下撑爆的。我干嘛死脑筋非要到内存占用超过80%才去打Dump呢呢呢???!

焕然大悟,如醍醐灌顶。

procdump w3wp -ma -m 8000 -o D:\Dumps (当内存超过8000M时抓取一个w3wp进程的完整Dump,并输出到D:\Dumps文件夹)

此时内存占用在40%左右,这次Dump终于成功创建了。

分析Dump,上WinDbg。如果对WinDbg不理解,可以看我这篇WinDbg学习笔记。

接下来就是一通命令乱敲,我尽量解释清晰。

使用<code>dumpheap -stat</code>命令查看当前所有托管类型的统计信息。从输出的结果来看:

其中占用内存最多当属<code>System.String</code>类型,接近4G的大小(是不是很吃惊?!)。

其次<code>System.Object[]</code>类型占有1.3G大小。

<code>Kingdee.BOS.JSON.JSONArray</code>类型也大概占用了560M。

我们首先来分析占用最多的<code>System.String</code>类型,看看有什么发现。

从上面的输出可以发现:

单个<code>System.String</code>类型最大占用2M以上。

超过200byte的字节的大小的<code>System.String</code>总大小也不过76M。(所以我们也不必深究大的String对象。)

那我们索性挑一个小点的对象来看看存储的是什么字符串,来满足一下我们的好奇心。

似乎是基础资料字段信息。那接下来使用<code>!gcroot</code>命令查看其对应的GC根,看看到底是什么对象持有其引用,导致占用内存得不到释放。

从以上输出可以看出:

该String类型被一个Hashset所持有。

从<code>Cache</code>关键字可以看出该String类型是被缓存所持有。

分析到这里,我们大致可以得出一个结论:

String类型占用4G内存,绝大多数是由缓存所占用,才导致String类型得不到释放。

那我们是不是可以猜测内存占用持续走高是不是被缓存撑爆的呢?。

带着这个疑问我们来继续分析下<code>Kingdee.BOS.JSON.JSONArray</code>类型。

从输出结果来看:

满屏都是40byte的JSONArray。只能使用<code>Ctrl+Break</code>命令中止输出。

但为了保险期间,我们来验证下有没有100byte以上的<code>JSONArray</code>。

这时我们可以大胆猜测所有的<code>JSONArray</code>对象都是40byte。从而可以得出另一个猜测占用560M内存的JSONArray,都具有相似的对象结构。接下来我们来验证这个猜测。随机选择几个对象,看看其内容具体是什么。

从输出可以看出:

JSONArray实质是<code>System.Object[]</code>类型。

对应的<code>MethodTable: 00007ffdb9386fc0</code>。

如果你记性好的话,我们应当还记得占用内存第二多的就是这个<code>System.Object[]</code>类型,占用1.3G。翻到上面,你可以发现其MethodTable和上面的统计信息是一致的。

(PS:到这里我们是不是可以猜测:<code>System.Object[]</code>占用的内存无法释放,就是由于被<code>JSONArray</code>持有引用导致的呢?)

既然是数组,就使用<code>!DumpArray</code> 命令来解开数组的面纱。

从以上输出可以看出,其共有8个子项,我们再随机挑几个子项看看是什么内容。

我们可以看到一个字符串内容是<code>FHTZDLB</code>,另一个是<code>发货通知单列表</code>。看到这,我立马就条件反射的想到,这不就是我们的菜单信息嘛。为了验证我的想法,连续查看几个<code>JSONArray</code>,都是相似的内容。

这时,我们继续发扬敢猜敢做的精神。是不是内存被菜单缓存撑爆的?!

为了验证这一猜测,我们继续从Dump中寻找佐证。使用<code>~* e!clrstack</code>来看看所有线程的调用堆栈吧。

通过仔细比对发现这么一条<code>Kingdee.BOS.App.Core.MainConsole.MainConsoleServer.GetMenuArrayForCache(Kingdee.BOS.Context)</code>调用堆栈。从方法命名来看,像是用来获取菜单数组并缓存。结合前后堆栈的联系,我们可以大致得出这样一个线索:用户使用WebApi登录后会缓存一份独立的菜单供用户使用。

有了代码堆栈,接下来知道怎么干了吧?当然是核实源代码确定问题啊。

<code>Kingdee.BOS.App.Core.MainConsole.MainConsoleServer.GetMenuArrayForCache(Kingdee.BOS.Context)</code>方法源代码如下:

我们发现它是用的<code>UserToken</code>来缓存用户菜单。看到<code>Token</code>,你可能就会条件反射的想到其生命周期。是的,聪明贤惠如你,Token是有生命周期的。也就意味着Token过期后,下次登录还会再次缓存一份菜单。你可能会问Token过期后没有去清对应的菜单缓存吗?是的,并没有。

严谨的你,可能又会问Token多久过期?20mins。你眼珠子一转,接着问,满打满算,一个用户1个小时也就申请3次Token,24小时,也就申请72个Token,一个菜单缓存也就顶多1K,所以一个用户一天也就最多占用72K。你的网站得有多少并发,才能被这么多菜单缓存撑爆啊?!

Good Question!!!

是的,客户的应用场景的并发也就顶多几百而已。那到底是什么导致如此多的菜单缓存呢?

原因是,客户的第三方客户端使用WebApi与我们的系统对接。而每次调用WebApi时都会先去调用登录接口,但却未保存会话信息。也就是说,客户第三方客户端每次的WebApi调用都会产生一个新的Token。那如果有成千上万的WebApi请求,也就意味着成千上万的菜单缓存啊。

好了,点到为止。至此,已经基本定位到问题的根源了。

也许很多同学没有接触过WinDbg,觉得其是一个复杂的工具。其实通过本文的案例讲解,其无非是通过一系列常见的命令来进行问题跟踪来定位问题。

最后来简单总结下,Windbg分析问题的步骤:

创建完整Dump文件

Windbg加载Dump文件

根据不同问题类型,使用相关的命令进行分析

耐心分析,抽丝剥茧

边分析边猜测边验证

结合源码验证猜想

修复验证

推荐链接:你必须知道的.NET Core开发指南

推荐链接:你必须知道的ML.NET开发指南

推荐链接:你必须知道的Office开发指南

推荐链接:你必须知道的IOT开发指南

推荐链接:你必须知道的Azure基础知识

推荐链接:你必须知道的PowerBI基础知识

Windbg分析高内存占用问题
<b></b> 关注我的公众号『微服务知多少』,我们微信不见不散。 阅罢此文,如果您觉得本文不错并有所收获,请【打赏】或【推荐】,也可【评论】留下您的问题或建议与我交流。 你的支持是我不断创作和分享的不竭动力!

作者:『圣杰』

出处:http://www.cnblogs.com/sheng-jie/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。