Python应用程序内存泄漏的调试

Quake Lee

quakelee@geekcn.org

新浪网技术(中国)有限公司

Sina Research & Development

Python-LDAP是什么?

Python-LDAP存在的问题

分析可能的泄漏点

可能性一:Python解释器内存泄漏

可能性二:Python代码未能释放必须
手动释放的资源

可能性三:TwistedCore存在内存泄漏

可能性四:某个Python扩展模块出现内存泄漏

内存泄漏调试过程

仍然泄漏

分析内存占用情况

分析结果

9676796 bytes in 235920 blocks are still reachable in loss record 32 of 32
   at 0x3C032183: malloc (vg_replace_malloc.c:105)
   by 0x80D9CA0: _PyObject_GC_Malloc (gcmodule.c:1248)
   by 0x80D9D6B: _PyObject_GC_NewVar (gcmodule.c:1279)
   by 0x80858D3: PyTuple_New (tupleobject.c:68)
   by 0x808D9FF: PyType_Ready (typeobject.c:3167)
   by 0x808D9C5: PyType_Ready (typeobject.c:3153)
   by 0x807C8DB: _Py_ReadyTypes (object.c:1805)
   by 0x80D1BC5: Py_InitializeEx (pythonrun.c:167)
   by 0x80D1F8C: Py_Initialize (pythonrun.c:283)
   by 0x80558C3: Py_Main (main.c:418)
   by 0x805520C: main (python.c:23)

难道是Python解释器泄漏了?

Python的Garbage Collector机制

透过GC看内存

吃内存的老鼠抓住了

这些列表那儿来的?

关于Python扩展模块

如何找到bug?

静下心来好好想

添加检查点

抓住bug了

针对每次调用添加检查点后,立刻就找到了泄漏点。正是在ldapsearch完毕, 获取ldap查询结果之后,出现了一个空白的列表。经过对那段代码进行分析, 仍然无法确认是那行代码写错了,因为对Python扩展模块操作引用计数器的 方法不是很熟悉。当改用同步方式查询ldap的时候,发现没有泄漏出空白列 表。通过比较这两种情况下代码的执行路径,发现在异步查询的情况下,在 返回的ldap控制码为空的时候,python-ldap返回了一个空白的列表给python 解释器,但是并没有讲这个空白列表的引用计数器减掉,导致这个列表的引 用计数一直不能清零,gc也不能回收这个对象。

问题就出在这里

灾难还没有过去

不要慌

再次祭出法宝-添加检查点

这次测试查不到的情况,左测右测,再也没有空白列表了…… 再次打出所有不能释放的对象,发现不能释放的对象并没有增加 也没有太多看起来可疑的僵尸对象。添加检查点失败……

再静心想想

推测结论

还记得Valgrind么?

valgrind这次真的救了我们。通过valgrind,我们发现python-ldap的c代码部分存在一个 严重的内存泄漏。当一个ldap记录没有查到的时候,一些struct在没有释放的情况下程序 就直接返回了。

没有内存泄漏的世界真好!

终于不再有内存泄漏了。调试上述问题,足足花了我将近2周的时间。我确实没有想到像 python-ldap这样还算比较常用,而且唯一的软件,还会出现这么多的问题。希望大家今后 自己在写程序的时候本着胆大心细的原则,不要给别人搞出这么多麻烦来。 另外,遇到麻烦也不要怨天尤人,任何问题都是会解决的,最好还是自己来:)

谢谢大家!

最后感谢大家,坚持听完了我的悲惨经历。另外需要感谢帮助我修复这些问题的重要人物 李鑫同学(delphij),valgrind这个工具就是他介绍给我的,况且那两个补丁也是李鑫同学 修改的,没有他的帮助,解决这个问题可能还需要花更长的时间。 还要感谢我的夫人,允许我找到这些问题,并且完成这份讲稿,她真的对我非常生气:P