1. 程式人生 > >urlopen內存泄漏淺析

urlopen內存泄漏淺析

__main__ 內存泄漏 一行 進行 reac handle 轉化 build ext

1.背景

  urllib,urllib2是客戶端http協議的實現,urllib2底層使用httplib,socket庫,它主要包含urlopen, build_opener, install_opener等func。python2.7使用urllib2庫中的urlopen會出現內存泄漏的現象,可以通過gc模塊來視察內存泄漏情況。

# -*- coding: utf-8 -*-
#!usr/bin/python
import urllib2
import socket
import gc

# check memory on memory leaks
def get_unreachable_memory_len():
    #當設置DEBUG_SAVEALL後,所有unreachable對象會append到garbage中,不會被銷毀,從而進行視察,測試時使用。
    gc.set_debug(gc.DEBUG_SAVEALL)
    gc.collect()
    unreachableL = []
    for it in gc.garbage:
        unreachableL.append(it)
        #print(str(it))
    print str(unreachableL)

def task():
    try:
        req = urllib2.urlopen(‘http://www.baidu.com/‘, timeout=3)
        text = req.read()
        #req.fp._sock.recv = None
        req.close()
    except urllib2.HTTPError, e:
        print e.code
    except urllib2.URLError, e:
        print e.reason
    else:
        print("urlopen success")

if __name__ == ‘__main__‘:
    get_unreachable_memory_len()
    print("-------------------------")
    task()
    print("-------------------------")
    get_unreachable_memory_len()

運行程序確定urlopen存在內存泄漏:技術分享圖片

2.現象分析

  python垃圾回收機制基於對象的引用計數,所以先找到造成循環引用的代碼。采用objgraph模塊打印出增加的對象。示例代碼如下:

# -*- coding: utf-8 -*-
#!usr/bin/python
import urllib2
import socket
import gc
import objgraph

# check memory on memory leaks
def get_unreachable_memory_len():
    #當設置DEBUG_SAVEALL後,所有unreachable對象會append到garbage中,不會被銷毀,從而進行視察,測試時使用。
    gc.set_debug(gc.DEBUG_SAVEALL)
    gc.collect()
    unreachableL = []
    for it in gc.garbage:
        unreachableL.append(it)
        #print(str(it))
    print str(unreachableL)

def task():
    try:
        req = urllib2.urlopen(‘http://www.baidu.com/‘, timeout=3)
        text = req.read()
        #req.fp._sock.recv = None
        req.close()
    except urllib2.HTTPError, e:
        print e.code
    except urllib2.URLError, e:
        print e.reason
    else:
        print("urlopen success")

#class HTTPResponse(object):
#    pass

if __name__ == ‘__main__‘:
    gc.set_debug(gc.DEBUG_SAVEALL)
    objgraph.show_growth()
    print("-------------------------")
    for i in range(5):
        task()
    print("-------------------------")
    objgraph.show_growth()

看到引用計數加5的三個字段,以及觀察到上一次運行結果首先出現的是httplib.HTTPResponse。

技術分享圖片

使用objgraph.show_backrefs對httplib.HTTPResponse進行分析:

# -*- coding: utf-8 -*-
#!usr/bin/python
import urllib2
import socket
import gc
import objgraph

# check memory on memory leaks
def get_unreachable_memory_len():
    #當設置DEBUG_SAVEALL後,所有unreachable對象會append到garbage中,不會被銷毀,從而進行視察,測試時使用。
    gc.set_debug(gc.DEBUG_SAVEALL)
    gc.collect()
    unreachableL = []
    for it in gc.garbage:
        unreachableL.append(it)
        #print(str(it))
    print str(unreachableL)

def task():
    try:
        req = urllib2.urlopen(‘http://www.baidu.com/‘, timeout=3)
        text = req.read()
        #req.fp._sock.recv = None
        req.close()
    except urllib2.HTTPError, e:
        print e.code
    except urllib2.URLError, e:
        print e.reason
    else:
        print("urlopen success")

#class HTTPResponse(object):
#    pass

if __name__ == ‘__main__‘:
    gc.set_debug(gc.DEBUG_SAVEALL)
    print("-------------------------")
    for i in range(5):
        task()
    print("-------------------------")
    objgraph.show_backrefs(objgraph.by_type(‘HTTPResponse‘)[0], max_depth = 10, filename = ‘obj.dot‘)

將生成的obj.dot轉化為obj.png(使用命令dot obj.dot -Tpng -o obj.png)圖示如下,記錄下造成循環引用的recv引用和read方法。

技術分享圖片

3.源碼追蹤

查看urllib2類圖可以使用pycharm自動生成UML類圖,這裏需要分析urllib2.urlopen的調用流程,可以引入pycallgraph模塊來分析,示例代碼入下:

# -*- coding: utf-8 -*-
#!usr/bin/python
import urllib2
import socket
import gc
from pycallgraph import PyCallGraph
from pycallgraph.output import GraphvizOutput

def task():
    graphviz = GraphvizOutput()
    graphviz.output_file = ‘urlopen.png‘
    with PyCallGraph(output=graphviz):
        try:
            req = urllib2.urlopen(‘http://www.baidu.com/‘, timeout=3)
            #text = req.read()
            #req.fp._sock.recv = None
            #req.close()
        except urllib2.HTTPError, e:
            print e.code
        except urllib2.URLError, e:
            print e.reason
        else:
            print("urlopen success")


if __name__ == ‘__main__‘:
    task()

截取部分生成的調用流程圖:

技術分享圖片

在HTTPHandler類中的do_open方法中有這一行代碼:

技術分享圖片

這個r指的是HTTPResopnse類,它只有read方法而沒有recv方法,這個引用在urlopen調用結束後並沒有釋放。解決內存泄漏問題就需要消除改引用。

4.解決方法:

1)上述示例當中調用task()之後使用gc.collect()進行手動內存回收。

2)http連接close之前手動解決r.recv這個引用。

 req = urllib2.urlopen(‘http://www.baidu.com/‘, timeout=3)
text = req.read()
#對於調用urlopen正常返回的情況手動解除r.recv = r.read這個引用
req.fp._sock.recv = None
req.close()

註:當返回錯誤狀態碼urllib2.HTTPError時無法生效,需要修改urllib2.py源碼為

技術分享圖片

3)改用更底層的socket,httplib庫。

參考資料:

1)http://python.jobbole.com/88827/

2)https://bugs.python.org/issue1208304

3)https://stackoverflow.com/questions/4214224/how-to-solve-python-memory-leak-when-using-urrlib2#

urlopen內存泄漏淺析