python urllib内存泄露问题分析

Posted by Shi Hai's Blog on February 23, 2023

一、问题背景

在python2/3中调用urllib模块会有循环引用没被释放,触发代码如下所示:

import gc
import sys


if sys.version_info.major == 3:
    from urllib import request
    urlopen = request.urlopen
else:
    import urllib2
    urlopen = urllib2.urlopen


gc.collect()
# the unreachable object will be freed if this flag not set
gc.set_debug(gc.DEBUG_SAVEALL)
print("before call urlopen(), the unreachable object len: %s" % len(gc.garbage))

a = urlopen('http://www.google.com')
del a
# check memory on memory leaks
gc.collect()
print("after call urlopen(), the unreachable object len: %s" % len(gc.garbage))

在python2.7执行示例代码可以看到如下输出:

before call urlopen(), the unreachable object len: 0
after call urlopen(), the unreachable object len: 21

在python3.9执行示例代码可以看到如下输出:

before call urlopen(), the unreachable object len: 0
after call urlopen(), the unreachable object len: 0

从触发用例输出结果初步看到python2.7的urllib2模块有内存泄露问题。

二、urllib2模块代码分析

修改测试代码对引用泄露进行分析,修改后的测试代码如下所示:

import gc
import pprint
import sys

import objgraph


if sys.version_info.major == 3:
    from urllib import request
    urlopen = request.urlopen
else:
    import urllib2
    urlopen = urllib2.urlopen


gc.collect()
# the unreachable object will be freed if this flag not set
gc.set_debug(gc.DEBUG_LEAK)
print("Before calling urlopen(), the unreachable object len: %s" % len(gc.garbage))

f = urlopen('http://www.google.com')
del f
# check memory on memory leaks
print("After calling urlopen(), collecting the unreachable objects: %s" % gc.collect())
#import pdb;pdb.set_trace()

输出结果如下所示,从这里看可以发现有7个不可达对象被gc回收掉。

Before calling urlopen(), the unreachable object len: 0
gc: collectable <HTTPResponse instance at 0x7f7393427820>
gc: collectable <dict 0x7f739342a830>
gc: collectable <HTTPMessage instance at 0x7f7393427910>
gc: collectable <dict 0x7f739342a5f0>
gc: collectable <list 0x7f7393475d70>
gc: collectable <list 0x7f7393d27c80>
gc: collectable <instancemethod 0x7f7393d17b90>
After calling urlopen(), collecting the unreachable objects: 7

gcdebug信息可以看出明显属于urllib模块的类是HTTPResponseHTTPMessage。此时我们可以将此测试代码最后一行打上断点,通过objgraph模块将不可达对象的拓扑图打印出来,执行语句: objgraph.show_refs(objgraph.at(0x7f7393427820), filename="20230225 0x7f7393427820.png", max_depth=5, too_many=30)

由于对象的属性可能非常复杂,所以我们需要调整objgraph.show_refs()函数中的max_depthtoo_many参数来不断扩展对象拓扑结构来找循环引用。这里不建议一开始就将max_depthtoo_many调的很大,可能会导致图片渲染时间较长,另外数据太多会看不清楚依赖细节。0x7f7393427820对象生成的完整图片如下所示(博客中无法放大,需要把图片才能缩放看到详细细节,补充了一个细节截图): 从这个全局图里面我们可以看见确实有一个循环引用: 在Python.2.7.18模块库中搜索urllib2httplib等相关模块可以发现,只有此截图处(在urllib2.py中的AbstractHTTPHandler类中)有recv属性的定义。 实际这行赋值定义操作移至上层基类HTTPResponse中定义就不会有循环引用问题。在HTTPResponse.read()函数后面并行的添加如下代码:

class HTTPResponse:
    def read(self, amt=None):
       #...
    recv = read

然后重新执行测试用例会发现循环引用消失了。

Before calling urlopen(), the unreachable object len: 0
After calling urlopen(), collecting the unreachable objects: 0

三、参考文献