摘要:最近在工作中遇到内存泄漏问题。运维同学紧急呼叫解决问题,所以在解决问题的同时,也系统的记录了内存泄漏问题的常见解决方法。最近在工作中遇到了内存泄漏的问题。运维同学紧急呼叫解决问题,所以在解决问题的同时,也系统的记录了内存泄漏问题的常见解决方法。首先搞清楚这个问题的现象:1、13号启动了一次服务,23号开始出现内存不断增加的问题。达到警告值并重启实例后,增长速度更快。2.服务部署在A和B两种芯片上,但除了模型推理,几乎所有的前处理和后处理都共用一套代码。B芯片有内存泄露警告,A芯片无异常。思路一:研究新旧源码的区别和二方库的依赖。基于以上两个条件,首先想到的是13号更新引入的问题,更新可能来自两个方面:1.自研代码2.二方依赖代码来自以上两个角度:一方面,我们将两个版本的源代码与Git历史资料和BeyondCompare工具进行了对比,重点阅读了A和B芯片代码中被分开处理的部分,没有发现异常。另一方面,通过piplist命令对比两个镜像包中的二方包,发现只有pytz时区工具依赖的版本发生了变化。经过研究分析,认为这个包导致内存泄露的可能性不大,暂且不谈。到目前为止,通过研究新旧版本的源代码变化来寻找内存泄漏的这条路,似乎有点走不下去了。思路二:监控新旧版本内存变化差异目前python常用的内存检测工具有pympler、objgraph、tracemalloc等,首先通过objgraph工具,旧版本中的TOP50变量类型观察并统计了新服务。\_growth(limit=30)这里为了更好的观察变化曲线,我简单做了一个封装,让数据直接输出到csv文件进行观察。stats=objgraph.most\_common\_types(limit=50)stats\_path="./types\_stats.csv"tmp\_dict=dict(stats)req\_time=time.strftime("%Y-%m-%d%H:%M:%S",time.localtime())tmp\_dict\['req\_time'\]=req\_timedf=pd.DataFrame.from\_dict(tmp\_dict,orient='index').Tifos.path.exists(stats\_path):df.to\_csv(stats\_path,mode='a',header=True,index=False)else:df.to\_csv(stats\_path,index=False)如下图,我用一批图片在新旧版本上跑了一个小时,一切都像老狗一样稳定,没有丝毫波动各种类型的数量。这时候我就想到平时在转试或者上线前会用一批格式异常的图片做边界校验。虽然这些异常在上线前肯定是被测试学员验证过的,但是死马当活马医,测试了一下。平静的数据就此被打破,如下图红框所示:dict、function、method、tuple、traceback等重要类型的数量开始持续攀升。这个时候镜像内存也在增加,没有收敛的迹象。所以,虽然无法确认是不是线上的问题,但至少已经定位到了bug。这时候回头查看日志,发现一个奇怪的现象:正常情况下,特殊图片引起的异常,日志应该会输出如下信息,即check\_image\_type方法只会打印一次异常堆栈。但现状是check\_image\_type方法循环打印了多次,重复次数随着测试次数的增加而增加。重新研究这里的异常处理代码。异常声明如下:异常代码如下:问题是经过思考问题的根源:这里的每一个异常实例都相当于被定义为一个全局变量,当抛出异常时,就是抛出变量的全局变量。当这个全局变量被压入异常栈并被处理时,它不会被回收。因此,随着错误格式图片调用次数的增加,异常堆栈中的信息也会增加。并且因为异常中还包含了请求的图片信息,所以内存会以MB级别增加。但是这部分代码已经上线很久了。如果真的是这部分线路的问题,为什么之前没有问题,为什么A芯片上没有问题呢?带着以上两个问题,我们做了两个验证:首先,我们确认这个问题在之前的版本和A芯片上也存在。其次,我们查看了在线通话记录,发现最近有一个新客户刚加入,大量类似问题的图片被用来调用某站点(大部分是B片)的服务。我们在网上找了一些例子,从日志中观察到同样的现象。至此,上面的问题已经基本解释清楚了。修复该bug后,内存溢出问题将不再出现。思路超前是有道理的,看来问题解决到这个地步就可以取消了。但是我问自己一个问题,如果一开始没有打印这行log,或者开发人员偷懒,没有打印所有的异常栈,我应该怎么定位呢?带着这样的疑问,我继续研究objgraph和pympler工具。上一篇文章已经确定图片异常的情况下会发生内存泄漏,那么我们重点关注一下此时发生的异常情况是什么:通过下面的命令,我们可以看到每次添加了哪些变量和增加的变量到内存中发生异常。记忆状态。1.使用objgraph工具objgraph.show\_growth(limit=20)2.使用pympler工具frompymplerimporttrackertr=tracker.SummaryTracker()tr.print\_diff()通过下面的代码,可以打印出这些供进一步分析参考的新变量。gth=objgraph.growth(limit=20)forgtingth:logger.info("growthtype:%s,count:%s,growth:%s"%(gt\[0\],gt\[1\],gt\[2\]))如果gt\[2\]>100或gt\[1\]>300:继续objgraph.show\_backrefs(objgraph.by\_type(gt\[0\])\[0\],max\_depth=10,too\_many=5,filename="./dots/%s\_backrefs.dot"%gt\[0\])objgraph.show\_refs(objgraph.by\_type(gt\[0\])\[0\],max\_depth=10,too\_many=5,filename="./dots/%s\_refs.dot"%gt\[0\])objgraph。show\_chain(objgraph.find\_backref\_chain(objgraph.by\_type(gt\[0\])\[0\],objgraph.is\_proper\_module),filename="./dots/%s\_chain.dot"%gt\[0\])通过graphviz的dot工具,将上面产生的图格式数据转换成下图:dot-Tpngxxx.dot-oxxx.png这里由于dict,list,frame,tuple、method等基本类型太多,不易观察,所以先在这里做过滤。内存中新增的ImageReqWrapper调用链内存中新增的traceback调用链:虽然有了前面的先验知识,我们自然会关注traceback及其对应的IMAGE\_FORMAT\_EXCEPTION异常。但是通过思考为什么上面这些本应该在服务调用结束后被回收的变量没有被回收,尤其是所有的traceback变量被IMAGE\_FORMAT\_EXCEPTION异常调用后都无法被回收;同时,再做一些小实验,相信很快就会定位到问题的根源。至此,我们可以得出以下结论:由于抛出的异常无法回收,相应的异常栈、请求体等变量也无法回收,且请求体中包含图片信息,所以每次这样的请求都会导致MB级内存泄露。另外,在研究过程中,还发现python3自带了一个内存分析工具tracemalloc。下面的代码可以用来观察代码行数和内存的关系。虽然不一定准确,但也能提供一些线索。importtracemalloctracemalloc.start(25)snapshot=tracemalloc.take\_snapshot()globalsnapshotgc.collect()snapshot1=tracemalloc.take\_snapshot()top\_stats=snapshot1.compare\_to(snapshot,'lineno')记录器.warning("\[Top20differences\]")forstatintop\_stats\[:20\]:ifstat.size\_diff<0:continuelogger.warning(stat)snapshot=tracemalloc.take\_snapshot()
