转载:http://www.jianshu.com/p/2d06a1a01cc3

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

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

ps -aux | grep 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-shell

pyrasite-shell <pid>

我先通过

gc.isenabled()

判断gc是否在工作, 结果发现是True, 也就是正常工作的, 而且使用gc.setdebug(gc.STATUS)设置gc为debug模式, 然后gc.collect()进行垃圾回收发现并没有更多内存释放,则否认了第二种泄露的可能.
现在来看gc.garbage中不能被释放的对象, 让我来检查一下是否有全局变量指向它们(这里极有可能是一个列表或者是一个字典)

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

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

>>> d = gc.garbage[-1]  # 随便找一个DicomFileLike对象
>>> d
<dicom.filebase.DicomFileLike object at 0x7f362c305390>
>>> objs = gc.get_referrers(d)
>>> len(objs)
8
>>> objs.remove(gc.garbage)
>>> objs.remove(locals())
>>> objs[0]
# 这里的输出是一个大字典, 包括了builtins, 应该是<pid>下的locals(). >>> objs[1]
<bound method DicomFileLike.write_leUS of <dicom.filebase.DicomFileLike object at 0x7f362c305390>> >>> objs[2]
<bound method DicomFileLike.read_leUL of <dicom.filebase.DicomFileLike object at 0x7f362c305390>> >>> objs[3]
<bound method DicomFileLike.read_leUS of <dicom.filebase.DicomFileLike object at 0x7f362c305390>> >>> objs[4]
<bound method DicomFileLike.write_leUL of <dicom.filebase.DicomFileLike object at 0x7f362c305390>> >>> objs[5]
<bound method DicomFileLike.read_le_tag of <dicom.filebase.DicomFileLike object at 0x7f362c305390>>

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

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

Does Python GC deal with reference-cycles like this?

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

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

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

def monkey_patch_dicom():
"""
修正dicom中DicomFileLike对象不释放内存问题
"""
del dicom.filebase.DicomIO.__del__

总结

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

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

  • 查看 进程的线程数量

    ps -o nlwp <pid>
  • 根据对象的id/address动态获取对象

    import ctypes
    obj = ctypes.cast(<addr_or_id>, ctypes.py_object).value
  • 查看垃圾回收的日志

    gc.set_debug(...)

作者:weidwonder
链接:http://www.jianshu.com/p/2d06a1a01cc3
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

记一次调试python内存泄露的问题的更多相关文章

  1. python内存泄露memory leak排查记录

    问题描述 A服务,是一个检测MGR集群主节点是否发生变化的服务,使用python语言实现的. 针对每个集群,主线程会创建一个子线程,并由子线程去检测.子线程会频繁的创建和销毁. 上线以后,由于经常会有 ...

  2. 排查python内存泄露中几个工具的使用

    本文主要介绍3个工具:pdb,objgraph,以及pympler. 1.pdb pdb是专门用于python代码调试,模仿gdb. 使用pdb可以查看堆栈,打印变量等. 这里介绍的是命令行下的pdb ...

  3. 关于排查python内存泄露的简单总结

    这次的内存泄露问题是发生在多线程场景下的. 各种工具都试过了,gc,objgraph, pdb,pympler等,仍然没有找到问题所在. pdb感觉用起来很方便,可以调试代码,对原来的代码无侵入性. ...

  4. python 内存泄露的诊断 - 独立思考 - ITeye技术网站

    python 内存泄露的诊断 - 独立思考 - ITeye技术网站 python 内存泄露的诊断 博客分类: 编程语言: Python Python多线程Blog.net  对于一个用 python ...

  5. 使用gc、objgraph干掉python内存泄露与循环引用!

    Python使用引用计数和垃圾回收来做内存管理,前面也写过一遍文章<Python内存优化>,介绍了在python中,如何profile内存使用情况,并做出相应的优化.本文介绍两个更致命的问 ...

  6. python内存泄露的诊断(转)

    本篇文章非原创,转载自:http://rstevens.iteye.com/blog/828565 . 对于一个用 python 实现的,长期运行的后台服务进程来说,如果内存持续增长,那么很可能是有了 ...

  7. python 内存泄露的诊断

    对于一个用 python 实现的,长期运行的后台服务进程来说,如果内存持续增长,那么很可能是有了"内存泄露" 一.内存泄露的原因 对于 python 这种支持垃圾回收的语言来说,怎 ...

  8. 记一次Java的内存泄露分析

    当前环境 jdk == 1.8 httpasyncclient == 4.1.3 代码地址 git 地址:https://github.com/jasonGeng88/java-network-pro ...

  9. python内存泄露查找

    1 前言: 1.1 像Java程序一样,虽然Python本身也有垃圾回收的功能,但是同样也会产生内存泄漏的问题 1.2 在Python程序里,内存泄漏是由于一个长期持有的对象不断的往一个dict或者l ...

随机推荐

  1. axis2客户端的几种调用方式

    (1)使用RPC方式调用WebService // 使用RPC方式调用WebService RPCServiceClient serviceClient = new RPCServiceClient( ...

  2. 将php中session存入redis中

    PHP 的会话默认是以文件的形式存在的,可以配置到 Redis 中,即提高了访问速度,又能很好地实现会话共享! 配置方式如下: 方法一:修改 php.ini 的设置 session.save_hand ...

  3. raw cannot be resolved or is not a field解决办法

    解决raw文件夹问题 查看左侧项目/res文件夹下是否有raw文件夹,(一定是放到res文件夹下,raw在项目开始创建时候不会自动创建,所以要自己创建)

  4. 合并百度影音的离线数据 with python 第二版 基于yield

    重新整理了一下代码. 增加了bdv,mkv的处理流程. 目前暂时支持windows平台. bdv,mkv,rmvb的不同处理流程 # -*- coding: UTF-8 -*- import os i ...

  5. AngularJS小练习20170508

    首先可能需要安装npm,并且配置环境. 1.打开Dos(命令提示符).按Windows徽标键+R组合键,输入cmd然后按回车键进入Dos. 2.安装Yeoman.在Dos下输入npm install ...

  6. PHP 下基于 php-amqp 扩展的 RabbitMQ 简单用例 (一) -- 安装 AMQP 扩展和 Direct Exchange 模式

    Windows 安装 amqp 扩展 RabbitMQ 是基于 amqp(高级消息队列协议) 协议的.使用 RabbitMQ 前必须为 PHP 安装相应的 amqp 扩展. 下载相应版本的 amqp ...

  7. 【C语言】控制台窗口图形界面编程(二)窗口信息和填充缓冲区

    目录 00. 目录 01. COORD结构体 02. SMALL_RECT结构 03. CONSOLE_SCREEN_BUFFER_INFO结构体 04. GetConsoleScreenBuffer ...

  8. 第1节 yarn:15、关于yarn中常用的参数设置

    第一个参数:container分配最小内存 yarn.scheduler.minimum-allocation-mb     1024   给应用程序container分配的最小内存 第二个参数:co ...

  9. mysql允许远程连接的命令

    mysql> use mysql; mysql> GRANT ALL ON *.* TO admin@'%' IDENTIFIED BY 'admin' WITH GRANT OPTION ...

  10. [Python3网络爬虫开发实战] 1.5.1-PyMySQL的安装

    在Python 3中,如果想要将数据存储到MySQL中,就需要借助PyMySQL来操作,本节中我们介绍一下它的安装方式. 1. 相关链接 GitHub:https://github.com/PyMyS ...