zoukankan      html  css  js  c++  java
  • python 内存泄漏调试

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

    Quake Lee

    quakelee@geekcn.org

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

    Sina Research & Development

    Python-LDAP是什么?

    • Python-LDAP是一个第三方的开源项目,主要目标是实现python的LDAP接口, 这是一个由C语言编写的Python扩展模块。
    • 该模块的主要功能是把通过C接口调用libldap从中取出的数据,转换成为Python的对象, 除此之外还有逆向转换

    Python-LDAP存在的问题

    • C接口调用程序内存泄漏
    • C接口程序Python引用计数器泄漏

    分析可能的泄漏点

    • 测试自己编写的程序时,发现程序严重消耗内存。在进行53万次查询后,程序占用的内存增大了4倍
    • 怀疑程序存在内存泄漏,经过测试,确认程序占用内存的数量的确随查询量增长而增长。确认存在内存泄漏。
    • 判断存在三种可能性:
      • Python解释器存在内存泄漏
      • 我们自己开发的Python程序未能及时释放必须手动释放的资源
      • TwistedCore存在内存泄漏
      • 调用的某个Python扩展模块出现内存泄漏

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

    • 调试步骤:
      • 首先检查了Python的Bug Manager,查看Python 2.4.1 以后,报告的和解决掉的关于Python的内存管理问题
      • Python近两年来只有3个关于内存泄漏方面的问题被人报告出来,两个是关于urllib2的。 另一个是关于dict的未能确认的泄漏,但只是出现在对dict进行特殊的使用时。
      • 据此判断在我的程序的使用方式下Python解释器出现内存泄漏的可能性很低。

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

    • 调试步骤:
      • 手动释放所有不需要持续使用的资源,再次测试
      • 内存占用没有变化,依然保持原样
      • 说明不是Python代码造成的内存泄漏

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

    • 调试步骤:
      • 查询了Twisted的Tickets系统
      • Twisted确实在近期出现过一些内存泄漏的情况,并且包括一个Twisted 2.1.0未修复的
      • 升级Twisted到Twisted 2.2.0
      • 再次测试,内存占用仍然没有变化,说明:
        • 可能TwistedCore不存在内存泄漏
        • 还存在未能发现的TwistedCore内存泄漏
      • Twisted代码比较多,结构复杂,先跳过这种可能性

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

    • 程序主要依赖的扩展模块就是Python-LDAP
    • 该模块存在内存泄漏的可能比较高
    • 如何进行调试呢?

    内存泄漏调试过程

    • 使用分析工具Valgrind对整个应用程序的运行过程进行分析
    • 运行命令行
      valgrind -v --leak-check=yes --num-callers=256 --logfile=d python d.py
    • 发现openldap 2.3.19中libldap.so包含两处内存泄漏
      • ldap初始化的时候解析域名时存在的一次性泄漏
      • 多线程并发情况下,断开连接时忘记释放的mutex

    仍然泄漏

    • 对第二个泄漏进行了修复
    • 再次测试,发现仍然泄漏
    • 此时valgrind的报告中只剩下一个一次性的泄漏了,这个泄漏只有49个字节
    • 内存都让谁占了呢?

    分析内存占用情况

    • 为valgrind增加命令行参数--show-reachable=yes,这将会把还在使用的内存情况也打印出来
    • 运行命令行
      valgrind -v --show-reachable=yes --leak-check=yes --num-callers=256 
      --logfile=d python d.py
      				

    分析结果

    • 长时间测试后发现了如下情况:
    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解释器的gcmodule控制
    • 难道是gcmodule的回收机制出现了问题?

    Python的Garbage Collector机制

    • Python的gc是分代垃圾回收机制,共分三代
    • 通过引用计数判断数据单元是否可以回收
    • 通过扩展模块gc中的接口可以分析调试垃圾回收的情况
    • 具体功能请参见Python手册

    透过GC看内存

    • Python的gc模块功能还是很强大的
    • 使用get_objects( )方法可以取得当前所有不能回收的对象(引用计数不为0)的列表
    • 存放在列表中的是所有对象的wrapper

    吃内存的老鼠抓住了

    • 通常情况下,不能释放的对象不会太多,启动时在2、3万个左右
    • 我发现程序长时间运行后,不可释放的对象总数大量增加。
    • 最终确认,不可释放的对象数量随查询量增大
    • 写个循环把他们都打印出来
    • 发现了大量的空白列表对象,并且这些空白的列表不能够被GC回收,只有一种可能性, 它们的引用计数不为0

    这些列表那儿来的?

    • 在Python代码中创建的对象,使用完毕后引用计数器就会归零
    • 我的代码中也已经手动删除了所有可以释放掉的资源,尤其是列表对象
    • 严重怀疑,这些东西来自于Python-LDAP扩展模块

    关于Python扩展模块

    • Python的扩展模块在操作Python对象之后,也需要改变对象的引用计数器
    • 扩展模块中的C程序一旦忘记修改或者不能正确的修改引用计数器,这些对象就成了僵尸对象

    如何找到bug?

    • python-ldap有2k多行C代码,虽说不是太多,但要是找起来也是大海捞针。
    • 对Python扩展模块的机制不是很熟悉,很难看出代码的错误
    • 给作者写信?这确实不是我们的风格,拿不出来patch,那儿好意思跟大家打招呼:)
    • 开发任务时间紧,靠别人不如靠自己
    • 不过问题还真是棘手,这种问题连valgrind也查不出来啦

    静下心来好好想

    • 出问题的应该肯定是Python-LDAP模块了
    • 我的程序调用它的次数并不是很多,每次查询调一次
    • 现在已经知道泄漏的对象是空白的列表
    • 于是...

    添加检查点

      • 在每个调用python-ldap的语句前后都添加检查点

    抓住bug了

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

    问题就出在这里

    灾难还没有过去

    • 再次进行测试,程序终于不再内存泄漏了,能够健康的运行了
    • 马上上线运行!
    • 程序再次长大……真是福无双至,祸不单行

    不要慌

    • 测试的时候,明明没有泄漏了,上线了又不行这是为什么?
      • 测试的时候,只测试了查的到的条件。
      • 上线后,多数情况下,是查不到这个结果的

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

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

    再静心想想

    • 这次有什么不同:
      • 不能释放的对象没有显著增加,甚至还比刚启动的时候减少了。
      • 打印所有的不能释放掉的对象,没有发现大量的相同对象

    推测结论

    • 结论:这次不是引用计数器泄漏

    还记得Valgrind么?

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

  • 相关阅读:
    MyCat 概念与配置
    使用zookeeper管理远程MyCat配置文件、MyCat监控、MyCat数据迁移(扩容)
    数据库分库分表中间件MyCat的安装和mycat配置详解
    搭建dubbo+zookeeper+dubboadmin分布式服务框架(windows平台下)
    数据库分库分表的类型和特点
    数据库为什么要分库分表
    正确安装MySQL5.7 解压缩版(手动配置)方法
    SQL 查询相同记录下日期最大的一条数据
    HashMap在Jdk1.7和1.8中的实现
    Spring Bean的生命周期
  • 原文地址:https://www.cnblogs.com/UnGeek/p/5984880.html
Copyright © 2011-2022 走看看