Python代码内存泄漏分析分析了一些内存泄漏的情况,对于实际排查,简单介绍了使用objgraph, gc等工具。结合实际遇到的一些情况,这里再来记录下实际在排查内存泄漏时遇到的问题。

内存去哪了

在服务器上观察Python进程的内存变化,当运行时发现Python进程不断增长时,潜在就有了泄漏的风险。通过objgraph等来排查Python对象泄漏时,往往需要在一个相对稳定的状态下才可以。如果当前进程繁忙,频繁的创建对象,即便objgraph中看到了数量众多的对象,也难以判定是否真的是泄漏。在当进程不再处理新的处理请求逻辑的时候,观察当前进程的内存占用,然后再通过objgraph去搜寻Python对象,容易发现一个问题。当前可以统计到的Python对象占据不了那么大的内存。这部分内存是去哪里了?

通过guppy可以去计算统计当前进程中Python对象消耗的内存大小,比对Top中看到的内存占用。通常会发现guppy中得到的内存大小远小于进程当前占据的内存。gdb-heap之类的工具也会返回类似的结果。那么那部分内存到底被什么占据着?实际上很可能只是内存被Python自身的分配机制占用着,参见diagnosing-memory-leaks-python。如何来确定这一点?

直接分析进程内存

这里可以找到一个dump进程内存的脚本,

#!/bin/bash

grep rw-p /proc/$1/maps | sed -n 's/^\([0-9a-f]*\)-\([0-9a-f]*\) .*$/\1 \2/p' | while read start stop; do gdb --batch --pid $1 -ex "dump memory $1-$start-$stop.dump 0x$start 0x$stop"; done

对导出生成的文件,通过下面命令,

strings -n 10 ...

来找到文件中长度超过一定范围的字符串内容,从内容数据中去反推是哪部分内容占据了内存。在实际存在内存泄漏情况下,heap对应的文件中应当能找到相关信息。在没有泄漏情况下,heap中的内容就很难分析出什么。之所以出现这种情况可能就是Python的内存机制。当这个问题比较明显的时候,可能隐含的问题是进程的内存分配过多,导致进程总是像系统申请内存,最终表现出的就是进程的内存不断上涨,但等到进程空闲时无法从objgraph中直接看出泄露的对象。

通过objgraph进行分析

回归到objgraph。既然问题的根源在Python代码中,那么应当还是可以通过objgraph来追寻踪迹。前文所说在进程空闲的时候去通过objgraph进行分析相对容易,但为了应对上述问题就需要在进程繁忙时介入。

objgraph的objgraph.show_most_common_types可以返回数量最多的类型信息。如果是自定义类型,那么问题很容易处理。如果是原生类型比如dictlist之类要怎么办?原生的类型往往数量众多,正常数据泄露数据混杂在一起。这种情况下不能够直接通过objgraph.find_backref_chain之类的方法去追寻引用链。一个很简单,但是很有效的方法就是将这些数据导出到文件,然后进行分析处理。如果能找到泄漏的数据,即便没有引用链,也容易去代码中分析。

简述下具体步骤,以dict数据为例。

  • 获取当前所有的dict对象
dict_list = objgraph.by_type('dict')
  • 将获取到的列表按顺序写入到文件中,一个dict输出一行,带上行号
def save_to_file(self, name, items):
  with open(name, 'w') as outputs:
    for idx, item in enumerate(items):
      try:
        outputs.write(str(idx) + " ### ")
        outputs.write(str(item))
        outputs.write('\n')
      except Exception as e:
        print e
    outputs.flush()
  • 实现脚本分析输出文件,结合grep、sed、awk命令进行处理。分析统计不同长度的dict有多少个,有多少dict存在相同结构(key相同)等。通过分析得到引起问题的dict。

  • 根据泄漏dict对应的行号信息,通过objgraph去获取引用关系,或者从代码中直接去分析。

objgraph.find_backref_chain(dict_list[idx], objgraph.is_proper_module)

总结

当进程内存不断增长且不归还系统时,通常一是真正存在泄漏,二是内存申请比释放多。前者相对容易处理。但处理起来的共通思路是去找寻引起问题的对象。如果通过工具如objgraph能直接定位到问题对象,那么最好。如果无法直接定位,那么就考虑笨办法吧,将数据导出进行分析处理。内存一定是被什么东西占用了,从数据上可以相对直观的发现问题。最后容易造成内存问题的通常就是全局单例、全局缓存、长期存活的对象,处理好了这部分代码,一般内存还是不容易有问题的。