记一次内存泄漏问题的排查经历

原文链接: https://mp.weixin.qq.com/s?__biz=Mzg4NzEyNDQxNw==&mid=2247483912&idx=1&sn=cf4b5c3566a2bcf95c8758faf9dbecf2&chksm=cf8e7aa6f8f9f3b01923c29e5a71cebe2f6bfa0825b5555f98cea34f1e7ddcda69b37b42b8aa&mpshare=1&scene=23&srcid=&sharer_sharetime=1571052685625&sharer_s

源宝导读:随着系统越来越庞大,越来越复杂,疑难杂症问题也越来越多。本文将介绍明源研发协同平台团队针对一次内存泄露问题的排查过程和技术探索。

一、背景

    内存泄漏,一个说大不大说下不小的瑕疵。作为开发者,我们都很清楚内存泄漏是我们代码问题导致的。但是话说回来,泄漏后果会很严重嘛?这不好说,如果对服务器内存的影响只有几个百分点,又或者对应用没有什么致命影响,那么修补内存泄漏就像鸡肋一样,“食之无味,弃之可惜”。反之内存泄漏也可能导致服务不可用,严重影响用户体验。

二、问题

    19年年初,协同平台后台服务开始间歇性非正常重启,每次重启耗时2-5分钟,重启期间服务不可用;并且事发前用户触发的部分任务没有重试机制,一些中间状态的数据需要人工修复。

    初步排查是程序池内存溢出引起的IIS自动回收,监控看板显示服务器内存从50%瞬间涨到100%,十几分钟之后触发回收,内存才彻底降下来。

    此后重启事件陆续不断,间隔从数小时到数天不等,每次重启前毫无征兆,没有规律可循,下图是服务的内存走势图。

三、事件分析

    通常内存泄漏最常规的做法是抓取Dump包,然后分析Dump包中的堆栈数据。但这次内存泄漏并非缓慢上升,而是瞬间将内存吃满,没有多余的硬件资源供抓包工具使用。更重要的是这个后台服务职责过重,也非常重要,属于一个在线系统,随便都有用户访问,抓包工具在进行抓包过程中会中断进程,影响服务使用。所以抓取Dump包的方案被暂时搁置。

    在翻遍所有日志没有找到蛛丝马迹之后,决定采用分治策略排除法,先将事故影响的范围慢慢缩小。

四、垂直拆分

    前文提到后台服务职责过重,需要进行拆分,这个是年初团队达成的共识,方案是先将后台作业任务独立出来,此次事件是个契机。新增一个调度服务,将现有定时作业和主动长时作业两块逻辑包含进去。拆分之后,主服务恢复正常,重启问题转移至新增的调度服务,风险随之降级,虽然问题没有解决,但是两个服务的事故级别是不一样的,主服务恢复正常后,至少不会对用户产生直接影响。

    虽然是一次投鼠忌器的尝试,好在结果符合预期,满足分治的需求,现在已经可以确定,元凶肯定就在调度服务的代码中。

五、监控埋点

    现在范围缩小了许多,定时作业和主动长时作业两大类,其中的子任务加起来不超过20个,将所有任务的开始和结束各记一次埋点。随后观察重启发作时间点与两大类后台作业的开始时间进行匹配,所有的任务都可以正常的开始,并正常的结束,没有一类任务的执行时间与重启时间点完全匹配,后台任务的嫌疑全部排出。

六、动态抓包

    排查陷入了僵局,再次观察监控看板,分析现有数据:调度服务的进程内存,正常值在400m-700m之间波动,出现异常时内存在2分钟内,由正常值上升至最高值,最高值接近系统总内存,这个与最初的症状有所不同,起初内存是90度直线瞬间吃满,现在是在2分钟内45度吃满内存。有了2分钟的缓冲期,可能与内存扩容有关,也可能与垂直拆分有关,原因不得而知,但这一点变化使得Dump抓包成为可能。

    这里推荐使用ProcDump,ProcDump是一个轻量级的Sysinternal团队开发的命令行工具,它的主要目的是监控应用程序的CPU内存异常动向,并在此异常时生成crash dump文件,供研发人员和管理员确定问题发生的原因。你还可以把它作为生成dump的工具使用在其他的脚本中。

    根据调度服务内存正常值的区间范围,内存阈值设置为1.5G,超过1.5g认为内存异常。按照ProcDump官网的介绍,ProcDump可以在阈值条件(内存、CPU、异常)到达时,自动触发Dump抓取,但是我部署在服务器ProcDump在一定时间后会出现假死现象,不能后台作业,也不知道下一次重启什么时候来临。无奈临时编写一个小工具,需求很简单,实时监控进程内存,10s轮询一次,超过阈值执行“procdump.exe -ma dotnet.exe“命令抓取Dump,下面是关键代码:

七、内存分析

    在小工具完成的第二天便成功捕捉到了传说的“黑匣子”,如空难事故中一样,它记录了程序在宕机前的性能指标。

    堆中数据显示,两个字符串数组占据了大部分内存空间,进一步跟踪到是一个zip包解压函数,在读取文件流时,while循环的退出条件判断有误,引起了死循环,无论系统内存有多大,都可以在短时间将服务器内存吃满。代码并非每次都会进来,具有一定的随机性,取决于用户的操作,所以重启没有规律。

    这段业务属于长时作业任务,在上面第五步的监控埋点中,记录了定时作业和主动长时作业两大类的执行结果,唯独这个被动长时作业任务成了漏网之鱼,没有纳入监控体系。

八、总结

    虽然修复这个Bug的工作量很小,但Bug持续时间长,影响较大。事后团队做了两点改进措施,一是研发流程中加入代码扫描工具,将这类“低级错误”拦截在开发阶段,避免提交到测试环境和生产环境;二是继续完善了现有监控数据,将被动长时作业任务的执行情况,展示在监控看板上,如果某个任务执行失败,或者只有开始没有结束,监控看板会及时发送提醒,为排查类似问题提供线索,快速定位类似问题。

猜你喜欢

转载自blog.csdn.net/zl1zl2zl3/article/details/102706127