关于Python的urllib2模块内存泄漏的问题以及解决方案
问题描述
(环境:Red Hat 7.3 / Python 2.7.5 )
这几天在Python2.7下使用Flask框架编写一个简单web应用。因为公司不允许应用程序直接访问数据库,只允许调用数据库模块提供的http接口,因此不能使用SQLAlchemy等模块,而是需要本程序主动发起Http请求。
在测试过程中,发现有比较严重的内存泄漏问题。经排查,内存泄漏被定位在使用urllib2的urlopen方法的代码块。泄漏与Flask框架无关。经查询后发现是个常见的问题。甚至多年前就有相关issue:
https://bugs.python.org/issue1208304
原本的代码如下:
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
try:
response = urllib2.urlopen(http_request, timout=5)
content = response.read()
response.close()
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
失败尝试
改用Requests模块
简单来说,没用。
代码确实变得美观了一点,但内存泄漏问题照旧。所以就不贴代码了。
在read()中加入数字参数
在我的环境下无效,并且会导致无法获取返回结果全文。
表示不是很理解这个解决方法的思路,希望有大佬解惑。
使用with/contextlib.closing
此处参考了@冰糖少女 的博文:
https://blog.csdn.net/TiffanyRabbit/article/details/80580056
参考原作者,写了个基于gc的垃圾判断,并使用with以及contextlib.closing进行回收。
新的代码如下:
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
# 显然内存泄漏不太可能出现在上面
try:
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
garbage_len_1 = len(gc.garbage)
# response = urllib2.urlopen(http_request, timout=5)
# content = response.read()
# response.close()
content = None
with closing(urllib2.urlopen(http_request, timout=5)) as response:
content = response.read()
gc.collect()
garbage_len_2 = len(gc.garbage)
if garbage_len_2 > garbage_len_1 :
print "垃圾增加" # 这里对垃圾量的判断不是很严谨
print garbage_len_2 - garbage_len_1
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
可惜的是在我的环境下并没有直接解决问题。
类似的,使用with特性的思路,运用在requests上同样无效。
强制去除变量循环引用
此处参考了 @张昊 先生的博文
http://www.find-bug.com/archives/8256
(gc模块更成熟一些的用法可以参考此博文)
以及 @ 安吉客 的博文
https://www.cnblogs.com/anjike/p/10230302.html
原理不详述,简单来说,内存泄漏的原因在于HttpResponse®的循环引用。因此只要利用赋值来强制去除变量的引用即可。
所以代码变成这个样子:
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
# 显然内存泄漏不太可能出现在上面
try:
response = urllib2.urlopen(http_request, timout=5)
content = response.read()
response.fp._sock.recv = None
response.close()
del response
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
改完后依然没有作用。加上上面一节加入的gc检测:
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
# 显然内存泄漏不太可能出现在上面
try:
gc.set_debug(gc.DEBUG_SAVEALL)
gc.collect()
garbage_len_1 = len(gc.garbage)
response = urllib2.urlopen(http_request, timout=5)
content = response.read()
response.fp._sock.recv = None
response.close()
del response
gc.collect()
garbage_len_2 = len(gc.garbage)
if garbage_len_2 > garbage_len_1 :
print "垃圾增加" # 这里对垃圾量的判断不是很严谨
print garbage_len_2 - garbage_len_1
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
运行后,看得出garbage_len_2 - garbage_len_1 == 0,但通过pidstat命令依然可以看出内存在增长。
问题解决
最后也算是稀里糊涂解决的。说实话我最后还是没完全搞明白为什么会出现这个情况。
先把可以正常运行的代码贴出来:
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
# 显然内存泄漏不太可能出现在上面
try:
response = urllib2.urlopen(http_request, timout=5)
content = response.read()
response.fp._sock.recv = None
response.close()
del response
gc.collect() # 这里是重点~~!
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
可能改变不是很明显。这里解释下:最终的代码其实是在加入 response.fp._sock.recv = None 后再在后面加一句gc.collect()。但是在此之前不能用gc.set_debug(gc.DEBUG_SAVEALL)。
对此更深入的解释我暂时没法给出,因为对gc没有源代码层次的了解。
鉴于这个情况,基于@冰糖少女 给出的解决方案,在最后加一句gc.collect():
def post_request(url, data):
headers = {'Content-Type': 'application/json'}
data_json = json.dump(data)
http_request = urllib2.Request(url=url, headers=headers, data=data_json)
# 显然内存泄漏不太可能出现在上面
try:
with closing(urllib2.urlopen(http_request, timout=5)) as response:
content = response.read()
gc.collect() # 重点在这
return json.loads(content)
except urllib2.URLError:
return '此处是默认输出'
# 更标准的写法是先except urllib2.HTTPError,此处不必体现
同样解决了内存泄漏的问题。
其他情况没有一个个去试,有空再补充。
最后补充一句:上述“正常运行”的代码未对抛出错误的情况进行内存回收,因此在大量触发错误时依然会有内存泄漏问题。因此在应用于生产环境时,需要对except情况进一步进行处理。
先写到这。