近期Imgsrc一处内存泄露问题的查找和解决
最近一直在查我们的imgsrc的内存泄露问题,事实上都是其所使用的ImageMagick库的bug。前些天又查了一个bug,涉及面较广,觉得有必要总结一下。
简要说明一下,imgsrc上部署的是apache模块,cdn通过其来访问tfs,并且做一些图像处理工作。有内存泄露是在线上发现的,内存不停的在涨。要找到问题所在,首先需要能够在线下重现,知道在什么情况下会泄露。线上系统当然不可能用valgrind来跑啦,还好我们有tcpcopy(赞一下网易的 @wangbin579 同学,真是个好东西),我们可以将线上流量镜像到跑着valgrind的机器上来,从而重现问题。跑了一晚上之后问题重现了,这时候需要做的是找到具体能触发问题的http请求。在访问日志和错误日志的帮助下,可以重放这些请求,这样就可以随时重现。一个晚上的访问日志有80多万条之多,我注意到其中有43条是在做图像处理时失败的。这些请求的原图往往都是一些不合法的或已损坏的图。先从这43个请求入手,运气不错,问题已经重现了。这告诉我们多注意一下不法分子总是好的。接下来就是使用2分法来找到具体的某一个访问,可以用脚本来干。最终确定是一个png图片。
经常用valgrind的人肯定知道,有时候打印出来的堆栈是不全的,会有一些是???。这次我也遇到了同样的问题,有人说用addr2line可以看到,试了一下无果。就上网查了下valgrind打印堆栈不全的原因,在这里有描述:http://valgrind.org/docs/manual/faq.html#faq.unhelpful
如果检查的程序是共享库,如果这个共享库在程序退出前被unload,那么valgrind会把它的debug信息给抛弃,导致调用堆栈那里会变成???的记录。解决方法是不调用dlclose。
首先是尝试将httpd源码apr库中的dso模块调用dlclose的地方注释掉,无果。想到这些堆栈还是有打出来的,之前没打出来的是ImageMagick的编解码库的堆栈,因此在ImageMagick的代码中搜了一下,果然有调用dlclose的地方。这个还不好改,尝试了几种方法(它有一个平台独立的ltdl库,由于调用dlclose的地方太多,我首先想到将dlclose函数给替换成一个空函数)都无果,甚至还导致进程不能正常启动。后来想到应该使改动尽可能小,就用SystemTap查看了一下kill httpd的过程中IamgeMagick调了dlclose的地方,然后将这两个地方的dlclose给注释掉,这样valgrind就打全调用堆栈了。
现在可以看到是具体哪个资源没有释放了,但是还不知道在什么情况下这个资源没有释放。
经过初步调试,可以断定问题出在循环读图片的行列像素这个过程中,现在的问题在于到底是这个过程中哪个函数、何时(哪次循环)出来的?首先得确定何时出来,然后在那个点调试就能知道是哪个函数出来的了。由于循环次数很多(800次),通过修改代码,在每次循环时都打一行日志到一个文件里头,这样就找到了出问题的循环点。然后在gdb中直接跳过正常循环,在那次循环过程中单步,确定了出问题的函数:这是一个libpng库里头的函数png_read_row,在执行完它之后就跟踪不了了。首先想到的是这个函数是否抛异常了,google了一下搜到一个我朝网友向ImageMagick反应的读png图时内存泄露的问题(http://www.imagemagick.org/discourse-server/viewtopic.php?f=3&t=20522),这哥们看来和我一样苦逼,不过这个哥们比我牛逼一点,知道png_read_row之后会跑到什么地方执行。事实上,他所反映的这个问题和我遇到的是同一个问题,不过是泄露的资源不同,ImageMagick的开发者们也不吸取教训。
ImageMagick(事实上是因为libpng这么要求)这里是使用setjmp/longjmp的机制来在读到损坏的png图片时释放资源的。顺便普及一下setjmp/longjmp机制:
这是C函数库提供用来全局跳转用的(goto只能函数内跳转),通常用于错误处理。setjmp函数用来设置一个跳转点,之后调用longjmp就能从其他函数跳转到刚刚调用setjmp的地方。setjmp函数有两种返回值,直接调用时会返回0,如果是从longjmp返回的则会返回非0。在实现上,setjmp会将当前栈的上下文信息保存在其参数(一个jmp_buf数据结构)中,然后longjmp会恢复指定的jmp_buf所保存的栈上下文信息,从刚刚调用setjmp的地方继续执行。注意,如果调用setjmp的函数返回了,那么保存的栈上下文信息就失效了。
ImageMagick在解码png前调用了setjmp(png_jmpbuf(ping)),这个png_jmpbuf应该是libpng库里面的一个全局变量。因此,可以推测libpng在其函数png_read_row中,如果遇到解码失败的情况,会调用longjmp(png_jmpbuf, 1),以便库的使用者进行错误处理。
我在setjmp返回非0的代码中设了个断点,果然进来了,调试发现虽然代码里面有释放资源的代码,但是并没有执行。怀疑是gcc O2优化的问题,修改ImageMagick的优化级别(改成O0),内存泄露就没有了。再仔细看了下代码,就发现问题了,gcc把这行释放资源的代码给优化掉了。代码如下:
这里ping_pixels就是引起泄露的资源,是在这之后的代码中分配的,因此一开始初始化为NULL,作者原意是要在之后如果通过longjmp回来之后对其进行释放。关键就在这里,longjmp返回之后,ping_pixels的值是多少?查看了相关资料,并且通过做简单的小实验之后总结如下:
放在内存中的变量,在longjmp返回时,仍然是调用longjmp这时候的值;而如果是放在寄存器中的变量,通过longjmp返回的时候,它的值会恢复成原来setjmp的时候的值。
这在《UNIX环境高级编程》一书中讲得非常清楚。
因此,这里我目前的解决方法是将ping_pixels变量的值变成volatile,这样gcc在优化时就不会将其放到寄存器中,也就不会在longjmp返回的时候恢复其为NULL。这里注意应该使用
unsigned char * volatile ping_pixels,而不是 volatile unsigned char* ping_pixels。
经过验证,这样修改之后,这个内存泄露就修复了。
建议继续学习:
- 了解前端内存泄露 (阅读:4459)
- GLIBC内存分配机制引发的“内存泄露” (阅读:3938)
- GC与JS内存泄露 (阅读:3456)
- 一个 Lua 内存泄露检查工具 (阅读:3015)
- PHP内存耗尽错误分析 (阅读:1653)
- 记一次内存泄露的debug过程 (阅读:1366)
- Android应用内存泄露分析、改善经验总结 (阅读:1188)
扫一扫订阅我的微信号:IT技术博客大学习
- 作者:明俨 来源: 淘宝核心系统团队博客
- 标签: Imgsrc 内存泄露
- 发布时间:2012-10-14 22:06:44
- [68] 如何拿下简短的域名
- [68] Go Reflect 性能
- [64] Oracle MTS模式下 进程地址与会话信
- [61] 图书馆的世界纪录
- [60] IOS安全–浅谈关于IOS加固的几种方法
- [60] 【社会化设计】自我(self)部分――欢迎区
- [58] android 开发入门
- [53] 视觉调整-设计师 vs. 逻辑
- [48] 读书笔记-壹百度:百度十年千倍的29条法则
- [47] 界面设计速成