关于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情况进一步进行处理。

先写到这。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值