本期内容
分享实战中内存泄漏解决思路。
Part1 初步分析原因
Part2 查找内存泄漏的根本原因
Part2.1 解决方案
Part3 总结
Part4 彩蛋
一、概要
大概在今年三月份的时候突然被紧急调到另外一个项目组解决线上内存泄漏问题。经过两周的玩命奋战终于解决了这个问题这里把心路历程及思路分享给大家。希望可以帮助到各位或现在正遇到这样事情的小伙伴提供一些思路。
二、场景
当部门老大找到我的时候,给我描述了这样一段话。
“目前服务出现了提交内存泄漏的问题,目前分析出来可能是日志组件有大量的日志消息堆积把内存占满导致服务崩溃了。在国内某地区客户的服务器上15000台物联网设备不能正常工作这个问题非常紧急需要马上解决。”
问题描述至此,没有其他可用信息。这时候我先崩溃了...但是任务找到你不能说不行。万一解决了这种重大事故还能在部门老大面前秀一把。
三、思路
(1)分析
Part1,分析日志堆积原因
- 拿到服务器地址去翻出日志文件,查看日志内容;内容基本上都是一些报错情况xxx对象为null,对象转换失败。
- 日志组件的实现也比较糟糕Log对象在每个调用的类里都会重新new
解决方案:
- 修复对象为null的问题并加上空值判断,大概的原因就是json值转换的时候传入的值是null那么就引起这两块的连锁反应。非常值得注意的一点是通常json对象转换的地方都会加入try块去捕获异常在程序里try的捕捉是会对.net程序造成性能影响的所以能用判断规避的尽量不要去触发try机制,程序性能被拖下去其他方面的处理就会变相的削减处理速度变慢那么数据堆积好像就解释的通了。
- 将日志组件重构为单例且线程安全的实现,写入日志的数据结构体是class这里改成struct,考虑的因素是引用类型会存在引用问题再就是考虑的值类型和引用类型在内存中占用的大小是不一样的,而且值类型和引用类型在处理速度上值类型更快。
以为这样就结束了吗?不,当程序改好之后放在测试服务器上跑第二天早上测试部的小姐姐就找到我说异常报错情况是好了,但是内存泄漏还是没解决。
Part2,查找内存泄漏的根本原因
看来Part1的操作仅仅只是修复了一个小bug而已,并不是我所想的那么简单,在日志的查看中还发现log日志中出现“tcp服务拒绝连接XXX异常”。当我看到这些的时候心情糟糕透了....
1.一早我就用Profile把服务程序跑了一遍发现了
- (1)有几个消息队列占用非常大,查阅代码之后发现服务端程序会和15000台物联网设备进行交互的所有数据都会先堆积到这个队列里如果这个队列满了(Queue上限被设定2w)会new新的Queue然后把溢出的部分转到新的Queue里,最可怕的是从队列里取数据的还是单线程处理。
- (2)还会有很多磁盘I/O的操作会存储在应用服务器本机上例如socket通讯的报文和需要转发的内容等等都会进行写入操作。
- (3)逐步调试的时候发现大部分的方法实现都是同步方法,而且框架版本居然是.net freamwork4。
解决方案:
(1)
- 【移除new新队列的机制、删除Main Queue的上限设置改为多线程处理Queue;一切数据堆积的本质就是数据处理不过来所以开辟再多的内存空间都是慢性死亡而已。】
- 【走访物联网硬件部门,询问物联网设备发送数据频率、设备数、单台设备发送单条数据的大小是多少KB;为什么需要了解?这些第一点在程序内记录日志然后统计成走势图能直接观察队列内部的变化开会的时候能给领导具有说服力的证据能看到数据量什么时候陡增、数据大小等;第二点因为这些报文数据需要存在应用服务器本地那么这时候就能计算出写入的数据量有没有超出普通硬盘的写入I/O瓶颈以及网络带宽的占用。】
- 【走访物联网硬件部门2,询问物联网设备socket传输数据时是否有走正常“tcp挥手”流程;为什么?因为socket tcp通讯中,是双工通道那么其中有一端突然断开,另一端会进入“wait”状态不会及时回收tcp连接资源,大家试想一下如果15000台设备高频短连接去操作那么服务端连接队列资源很有可能吃不消。这个时候就需要服务端主动断开“失效”连接及时回收资源“拆除双工通道”以及调整socket连接队列大小。】
(2)磁盘写入报文信息这块,就要用三寸不烂之舌说动项目经理把这块砍掉以节约CPU性能以及减少磁盘I/O,大伙试想一下每次socket通讯进行收发的时候都要去操作一下I/O那是多么恐怖的一件事情;最后沟通结果那个组的项目经理同意砍掉部分模块磁盘写入功能,那么问题来了剩下的怎么办如何将优势进一步扩大?这时候继续查阅项目代码,结果发现socket通讯中“收”、“发”都会操作一次。那么这时候需要做的是将报文积累到一定数量比如说积累1000条报文再一次性写入那么磁盘I/O的操作频率将成倍递减。
(3)最后一个问题,就是讲所有的方法修改为异步方法。这时候就能祭出Task、Async、Await了。但是基于的框架是.net freamwork4的,后来又去查阅MSDN的文档发现.net freamwork4远古框架中还是有这些特性的虽然用法稍微难受点但是还是能优化的。一定要记住一点,开发服务端要有“服务端”思维如果都是同步方法就会被同步阻塞处于“等待处理结果状态”这样的话服务端的并发量是上不去的。
这里虽然没怎么用上的一发大招,但是这里还是分享给大家“注释大法”;注释掉最有可能出问题的地方逐一排查一定能发现问题的所在就是非常的耗时那会我基本每天工作12小时,尤其是公司的远古项目通常“代码烂”、“设计基本没有”、“使用的.net框架版本低”等等,一堆恶心人的事情发生。
(2)工具
- Visual Studio自带的Profile。【可以分析CPU、内存等占用情况;这款比较推荐】
- VMMap【可以分析CPU、内存等占用情况】
- ANTS Performance Profiler【这款工具比较强大能分析调用链路逐级告诉你内存占用的地方以及内存占用大小】
- Window操作系统自带的资源监视器这个不用多说大家都会用。
Part3,总结
基于以上的修改,在测试服务器上稳定运行3周内存稳定在2.9G左右;
一定要记住:
- “遇到任何棘手的事情不要抱怨。”
- “一个优秀的软件工程招聘进来就是解决问题的,而不是制造问题;”
- “对于任务的安排,高手永远都是说出解决问题的期限;到点交东西。而不是支支吾吾说不清楚、退缩。”
- “遇到问题冷静思考,相信自己一定可以的;那怕失败去尝试一下也好。”
- “没解决问题的时候不要说任何话,说什么都像是在找理由。闭上嘴巴去想办法。”
其实解决这个问题时期发生了很多有趣的故事,不过最终还是要解决难啃的问题证明自己,开发学习本身就是一个不断变强的过程“修技术,也修内心”当自己逐渐变强之后也不要鄙视技术不好的同事始终保持一颗学徒的心。
Part4,彩蛋
解决这个问题之后在同部门同事的眼里威望都会有提升(尤其是测试部门的小姐姐,因为她们不用费力的每天去看服务器了),最终解决项目的重大事故部门老大给了机会调到其他省的研发中心当项目经理薪资平移的基础上再上浮百分之十。可见掌握一手救急的技能有多么划算。