技术头条 - 一个快速在微博传播文章的方式     搜索本站
您现在的位置首页 --> Apache --> 近期Imgsrc一处内存泄露问题的查找和解决

近期Imgsrc一处内存泄露问题的查找和解决

浏览:5031次  出处信息

    最近一直在查我们的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把这行释放资源的代码给优化掉了。代码如下:

  • unsigned char
  •     *ping_pixels;
  •   ping_pixels=(unsigned char *) NULL;
  •     

  •   if (setjmp(png_jmpbuf(ping)))
  •     {
  •       /*
  •         PNG image is corrupt.
  •       */
  •       png_destroy_read_struct(&ping,&ping_info,&end_info);
  •     

  • #ifdef PNG_SETJMP_NOT_THREAD_SAFE
  •       UnlockSemaphoreInfo(ping_semaphore);
  • #endif
  •     

  •       if (ping_pixels != (unsigned char *) NULL)
  •         ping_pixels=(unsigned char *) RelinquishMagickMemory(ping_pixels);
  •     这里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。

        经过验证,这样修改之后,这个内存泄露就修复了。

    建议继续学习:

    1. 了解前端内存泄露    (阅读:4284)
    2. GLIBC内存分配机制引发的“内存泄露”    (阅读:3835)
    3. GC与JS内存泄露    (阅读:3330)
    4. 一个 Lua 内存泄露检查工具    (阅读:2899)
    5. PHP内存耗尽错误分析    (阅读:1247)
    6. 记一次内存泄露的debug过程    (阅读:1147)
    7. Android应用内存泄露分析、改善经验总结    (阅读:985)
    QQ技术交流群:445447336,欢迎加入!
    扫一扫订阅我的微信号:IT技术博客大学习
    © 2009 - 2024 by blogread.cn 微博:@IT技术博客大学习

    京ICP备15002552号-1