专栏首页北京马哥教育记一次调试python内存泄露的问题

记一次调试python内存泄露的问题

这两天由于公司需要, 自己编写了一个用于接收dicom文件(医学图像文件)的server. 经过各种coding-debuging-coding-debuging之后, 终于上线了, 上线后心里美滋滋的, 一切正常.

第二天一上班, 负责人和我说接收太慢了, 卡的要死. 我想难道是python本身的问题?(程序员本征思维)我好奇的打开了终端输入

找到进程id:

即 21610

我这里还没传几张图片就到78m了, 看来是内存问题. 其实生产环境占用更多, 因为生产环境保密所以只能在测试环境测试比较少的数据, 生产环境曾一度上升到3.7g的内存占用.

这样果断不行啊. 我发现有新的文件上传之后内存占用就会增大, 初步断定是dicom文件相关对象占用的内存. 现在的首要工作就是找到一个能进行内存泄露的调试工具了.

说道这里可能大家会有疑问, python作为动态类型语言同时拥有垃圾回收机怎么会有内存泄露? 其实也有可能出现内存泄露的情况, 有如下几种:

  1. 对象一直被全局变量所引用, 全局变量生命周期长.
  2. 垃圾回收机被禁用或者设置成debug状态, 垃圾回收的内存不会被释放.
  3. 也是非常罕见的内存泄露的方式就是今天遇到的问题, 我周旋这个问题两天才debug出来, 现在分享给大家.客官请您继续往下看

说到查看python内存泄露的工具, 其实有挺多, 现在简短介绍一下

  • gc: python 内置模块, 函数少功能基本, 使用简单, 作为python开发者里边的内容必须过一遍
  • objgraph: 可以绘制对象引用图, 对于对象种类较少, 结构比较简单的程序适用, 我这个一个库套一个库, 内存还用的这么多,
  • guppy: 可以对堆里边的对象进行统计, 算是比较实用
  • pympler: 可以统计内存里边各种类型的使用, 获取对象的大小

上边这些虽然有用但是总是搞不到点子上, 上边这些都需要改我的源程序, 比较费劲, 线上的代码不是说改就能改的, 而且他们功能也都比较弱, 后来发现两个强大的工具:

  • tracemalloc: 究极强, 可以直接看到哪个(哪些)对象占用了最大的空间, 这些对象是谁, 调用栈是啥样的, python3直接内置, python2如果安装的话需要编译
  • pyrasite: 牛逼的第三方库, 可以渗透进入正在运行的python进程动态修改里边的数据和代码(其实修改代码就是通过修改数据实现)

我开始的时候非常想用tracemalloc, 可是对python2特别不友好, 需要重新编译python, 而且只能用python2.7.8编译, 编译好了也不容易嵌入到虚拟环境中, 头大, 果断换第二个.

注: pyrasite使用之前需要在root用户下运行命令 echo 0 > /proc/sys/kernel/yama/ptrace_scope后才能正常使用

pyrasite里边有一个工具叫pyrasite-memory-viewer, 功能和guppy差不多, 不过可以对内存使用统计和对象之间的引用关系进行快照保存, 很易用也很强大.运行

pyrasite-memory-viewer <pid>

可以看到占用内存最多的是DicomFileLike这种类型的对象.已经达到上万个, 这是不能忍受的.

就目前来看可能会有上边说的两种内存泄露原因导致不能回收这个对象.打开

pyrasite-shellpyrasite-shell <pid>

我先通过

gc.isenabled()

判断gc是否在工作, 结果发现是True, 也就是正常工作的, 而且使用gc.setdebug(gc.STATUS)设置gc为debug模式, 然后gc.collect()进行垃圾回收发现并没有更多内存释放,则否认了第二种泄露的可能.

现在来看gc.garbage中不能被释放的对象, 让我来检查一下是否有全局变量指向它们(这里极有可能是一个列表或者是一个字典)

gc.garbage 可以看到被塞满了各种DicomFileLike对象

所以我们的目的就是先找到一个对象然后一级一级的向上寻找相互的引用.

到这里发现其实没有更多的全局变量指向这个d了, 而且发现所以有的方法的对象地址和d是相同的, 说明了这个对象其实是自循环引用的.

那么python不可能不支持循环引用对象的回收吧? 跟着这个问题我查了一下stackoverflow

Does Python GC deal with reference-cycles like this?

这个问题的第一个回答介绍的很清楚了, 如果用户不自定类的__del__方法, gc可以回收带有自引用的对象, 但是你自己实现了__del__方法就不行了.

这就是python内存泄露的第三个可能.

回头看DicomFileLike的源码, 果然在__init__函数上方定义了一个__del__函数, 我这里使用了一个猴子补丁删除了这个方法, 内存泄露的问题就得以解决了.

总结

到这里整个调试过程就结束了, 然而实际上过程中做了很多曲折的工作, 在pyrasite中会找到几个引用DicomFileLike对象的object, 比较不容易辨别, 最开始我以为是某个全局的对象引用的DicomFileLike, 比如是列表什么的, 后来发现其实是locals()和globals()字典, 如果使用pyrasite-memory-viewer保存下来的数据会发现有一个大列表指向所有没有回收的DicomFileLike对象, 捯饬半天发现其实是gc.garbage, 好囧, 曾让我一度怀疑是第一种泄露方式, 但是怎么找这个对象都没有找到. 其中还有几次看到线程达到140+, 后来发现其实和线程一点关系没有, 线程维持在这个数目上边很稳定.

在这个过程中用到的其他几个hack的技巧有:

查看进程的线程数量

根据对象的id/address动态获取对象

查看垃圾回收的日志

作者:weidwonder 来源:http://www.jianshu.com/p/2d06a1a01cc3


本文分享自微信公众号 - 马哥Linux运维(magedu-Linux),作者:weidwonder

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-12-18

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 深入了解一下Redis的内存模型!

    小小科
  • Linux内存被吃掉了,它去哪里了?

    在Windows下资源管理器查看内存使用的情况,如果使用率达到80%以上,再运行大程序就能感觉到系统不流畅了,因为在内存紧缺的情况下使用交换分区,频繁地从磁盘上...

    小小科
  • 还不会Python正则表达式?看这篇文章试试

    今天写爬虫偶然想到了初学正则表达式时候,看过一篇文章非常不错。检索一下还真的找到了。 re模块 re.search 经常用match = re.search(p...

    小小科
  • OC学习10——内存管理

    1、对于面向对象的语言,程序需要不断地创建对象。这些对象都是保存在堆内存中,而我们的指针变量中保存的是这些对象在堆内存中的地址,当该对象使用结束之后,指针变量指...

    mukekeheart
  • jvm 堆外堆内浅析

    HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷...

    开发架构二三事
  • Android内存泄漏分享

    内容概述 内存泄漏和内存管理相关基础。 Android中的内存使用。 内存分析工具和实践。 以下内容不考虑非引用类型的数据,或者将其等同为对应的引用类型看待——...

    用户1172465
  • 使用commons-pool管理FTP连接

    在封装一个FTP工具类文章,已经完成一版对FTP连接的管理,设计了模板方法,为工具类上传和下载文件方法的提供获取对象和释放对象支持。

    用户3579639
  • 堆外内存概要

    DirectByteBuffer JDK中使用 DirectByteBuffer对象来表示堆外内存,每个 DirectByteBuffer对象在初始化时,都会创...

    于霆霖
  • 附件3:eclipse memory a

    http://jingyan.baidu.com/article/ce09321b620a3d2bff858ff5.html

    用户2398817
  • C# 内存管理机制及 WP 内存泄漏定位方法

    C#内存管理机制及WP内存泄漏定位方法 一、C#的内存管理机制 1. 托管资源与非托管资源 什么是托管资源?托管资源通俗的理解就是,把资源交给.net去管理,这...

    微信终端开发团队

扫码关注云+社区

领取腾讯云代金券