Python内存泄露调查

第一步:本地复现问题

本地测试一发,确实能稳定复现…

无法判断框架还是自己写的Code 的问题。

第二步:搜索:python 怎样排查内存泄漏

开始搜索一些,比如这个 Python 内存泄漏调试指导思想 看来看去,这些文章大致分为两类,一部分是介绍 Python 垃圾回收机制;另一部分是介绍公众调试工具的使用。 有的文章里面还总结了常见的出现内存泄露的情景:

  1. C 语言编写的模块出现内存泄漏
  2. 全局对象比如 list/dict 不断增大
  3. 代码内有引用循环,python 垃圾回收机制没办法回收

前两种情况,没什么好说的,对于第三种情况,这里补充一个例子

1
2
3
4
5
6
7
a=[]
b=[]
a.append(b)
b.append(a)
del a
del b
print gc.collect()

​ Python中的对象主要分为类型对象和实例对象, 除了内置的类型对象外,都存在于堆上,内置的类型对象则静态分配内存. 为int分配的内存是永远也不会被python释放的,所有的int对象使用的内存大小和同时存在的int数量的最大值有关.str对象也存在同样的问题。

参考:

Python 垃圾回收三种方式

  • 引用计数(主);

  • 标记-清除(效率不高);

  • 分代收集。

    以下情况时,对象的引用计数增加:

  1. 对象被创建;
  2. 另外的别名被创建;
  3. 作为参数传递给函数;
  4. 成为容器对象的一个元素;

以下情况时,对象的引用计数减少:

  1. 一个本地引用离开其作用范围,比如函数结束时,所有局部非循环引用变量都被自动销毁;
  2. 用del语句显式删除一个变量(同时该变量从name space中删除);
  3. 对象的一个别名被赋值给其他对象;
  4. 对象被移出一个容器对象时;
  5. 容器对象本身的引用计数变成0;

​ 有 del() 函数的对象间的循环引用是导致内存泄漏的主凶。 另外需要说明:对没有 del() 函数的 Python 对象间的循环引用,是可以被自动垃圾回收掉的。

​ 对于大的数组解决的方式,基本只有用yield来将列表对象转换为生成器对象。列表对象会同时生成所有元素,从而直接分配所有内存。而生成器则是一次生成一个元素,比较节约内存。

各种调试工具试用分析

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

个人最终使用感受:

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

  • 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差不多, 不过可以对内存使用统计和对象之间的引用关系进行快照保存, 很易用也很强大。

第三步:各种照搬和尝试,找到原因

使用各种工具和方法查内存使用情况和代码运行状态,直到查出问题所在。

查看 进程的线程数量

1
ps -o nlwp <pid>

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

1
2
import ctypes
obj = ctypes.cast(<addr_or_id>, ctypes.py_object).value

*查看垃圾回收的日志 *

1
gc.set_debug(...)

第四步:思考总结,回顾过程