近期開發了一個小型Web應用,使用了uWSGI和web.py,遇到了一個內存泄露問題折騰了很久,記錄一下,但願能夠幫助別人少踩坑。html
P.S. 公司項目,我不能把完整代碼貼上來,因此大部分是文字說明,如下配置文件中的路徑也是虛構的。前端
Ubuntu 13.10python
uWSGI 1.9.13nginx
web.py 0.37web
sqlite3 3.7.17 2013-05-20sql
nginx 1.4.7數據庫
nginx配置做爲Web前端,經過domain socket和uWSGI服務器交互:服務器
server { listen xxxx; access_log off; location / { uwsgi_pass unix:///tmp/app/uwsgi.sock; include uwsgi_params; } }
uWSGI配置:app
[uwsgi] app_path = /spec/app log_dir = /log/app tmp_dir = /tmp/app master = true processes = 4 threads = 2 pidfile = %(tmp_dir)/uwsgi.pid socket = %(tmp_dir)/uwsgi.sock chdir = %(app_path) plugin = python module = index daemonize = %(log_dir)/uwsgi.log log-maxsize = 1000000 log-truncate = true disable-logging = true reload-on-as = 30 reload-on-rss = 30
該應用使用了uWSGI提供的定時器功能來執行定時任務,發現運行一段時間後就會有內存泄露。仔細觀察發現,即便沒有外部請求,也會有內存泄露;有時候外部請求會使得泄露的內存被回收。dom
在應用中使用uWSGI的定時器功能的代碼以下:
import uwsgi # add timers timer_list = [ # signal, callback, timeout, who (98, modulea.timer_func, modulea.get_interval, ""), (99, users.timer_func, 60, ""), ] for timer in timer_list: uwsgi.register_signal(timer[0], timer[3], timer[1]) if callable(timer[2]): interval = timer[2]() else: interval = timer[2] uwsgi.add_timer(timer[0], interval)
由於以前使用過一樣的環境開發了另外一個應用,沒用使用uWSGI的定時器,因此懷疑內存泄露是定時器致使的。
首先,刪掉定時器後,發現uWSGI進程不會發生內存泄露了。肯定是定時器中的代碼致使的內存泄露。
而後把定時器中的代碼放到一個請求處理函數中去執行,經過構造HTTP請求來觸發代碼執行。結果是沒有內存泄露。所以,結論是同一段代碼在定時器中執行有內存泄露,在請求處理代碼中執行沒有內存泄露。
這個實驗也把致使內存泄露的代碼鎖定到了users.timer_func
函數中,其餘函數都沒有內存泄露問題。
users.timer_func
函數只做了一件事情,就是從sqlite3數據庫中讀取用戶表,修改全部用戶的某些狀態值。先來看下代碼:
def update_users(): user_list = Users.objects.all() if user_list is None: return for eachuser in user_list: # update eachuser's attributes ... # do database update eachuser.update() def timer_func(signal_num): update_users()
Users類是一個用戶管理的類,父類是Model類。其中的Users.objects.all()
是經過Model類的一個新的元類實現的,主要代碼以下:
def all(self): db = _connect_to_db() results = db.select(self._table_name) return [self.cls(x) for x in results]
也就是利用web.py的數據庫API鏈接到數據庫,而後讀取一張表的全部行,把每一行的都實例化成一個Users實例。
綜上所述,致使內存泄露的users.timer_func
函數主要的操做就是建立數據庫鏈接,而後讀寫數據表。這個時候,咱們能夠猜想內存泄露多是數據庫鏈接沒關致使的,由於咱們本身建立的Users實例在函數退出後應該都被回收了。
如何驗證這個猜想呢?由於sqlite數據庫是文件型數據庫,進程中每一個鏈接至關於打開一個文件描述符,因此可使用lsof命令查看uWSGI到底打開了多少次數據庫文件:
# 假設2771是其中一個uWSGI進程的PID $lsof -p 2771 | grep service.db | wc -l
經過不斷執行這個命令,咱們發現以下規律:
若是是在定時器中執行數據庫操做,每次執行都打開數據庫文件一次,可是沒有關閉(上述命令輸出的值在增長)
若是是請求處理函數中執行數據庫操做,則數據庫文件被打開後會被關閉(上述命令輸出的值不變)
到這邊咱們能夠確認,泄露的是數據庫鏈接對象,並且只有在定時器函數中才會泄露,在請求處理函數中不會。
這個問題困擾了我好久。最後採用最笨的辦法去解決 -- 閱讀web.py的源碼。經過閱讀源碼能夠發現,web.py的數據庫操做主要代碼是在class DB
中,真正的數據庫鏈接則存放在DB類的_ctx
成員中。
class DB: """Database""" def __init__(self, db_module, keywords): """Creates a database. """ # some DB implementaions take optional paramater `driver` to use a specific driver modue # but it should not be passed to connect keywords.pop('driver', None) self.db_module = db_module self.keywords = keywords self._ctx = threadeddict() ... def _getctx(self): if not self._ctx.get('db'): self._load_context(self._ctx) return self._ctx ctx = property(_getctx) ...
其餘具體操做代碼就不貼了,這裏的關鍵信息是self._ctx = threadeddict()
。這說明了數據庫鏈接是thread local對象,即線程獨立變量,在線程被銷燬時會自動回收,不然就一直保存着,除非手動銷燬。能夠查看Python的threading.local
的文檔。因而,我開始懷疑,是否是uWSGI的定時器線程一直沒有銷燬,而處理請求的線程則是每次處理請求後都銷燬,致使了數據庫鏈接的泄露呢?
爲了證明這個猜測,繼續做實驗。此次用上了gc模塊(也能夠用objgraph模塊,不過這個問題中gc已經夠用了)。將下面代碼分別加入到定時器函數中和請求處理函數中:
objlist = gc.get_objects() print len([x for x in objlist if isinstance(x, web.ThreadedDict)])
而後咱們能夠在uWSGI的log中看到ThreadedDict
的統計值。結果果真如咱們所猜測的:不斷執行定時器函數會讓這個統計值不斷增長,而請求處理函數中則不會。
因此,咱們也就找到了數據庫鏈接泄露的緣由,也就是內存泄露的緣由:uWSGI中定時器函數所對應的線程不會主動銷燬thread local數據,致使thread local數據沒有被回收。
因爲每一個uWSGI進程可能只開啓一個線程,也可能有多個線程,所以能夠總結的狀況大概有以下幾種:
只有一個線程時:若是該線程一直在運行定時器函數,則在此期間該進程不會從新初始化,thread local對象不會被回收。當該線程處理請求時,會從新初始化線程,thread local對象會被回收,釋放的內存會被回收。
當有多個線程時:每一個線程自身的狀況和上面描述的一致,不過有可能出現一個線程一直在運行定時器函數的狀況(也就是內存一直泄露)。
在定時器函數退出前,清除web.py存放在thread local中的對象。代碼以下:
def timer_func(signal_num): update_users() # bypass uWSGI timer function bug: timer thread doesn't release # thread local resource. web.ThreadedDict.clear_all()
P.S.
該方法目前還沒發現反作用,若是有的話那就是把別人存放的數據也給清除了。
其餘版本的uWSGI服務器沒測試過。
處理請求的線程爲何就能夠主動銷燬thread local的數據呢?難道uWSGI對不一樣的線程有區別對待?其實不是的,若是一個線程在處理HTTP請求時,會調用WSGI規範定義的接口,web.py在實現這個接口的時候,先執行了ThreadedDict.clear_all()
,因此全部thread local數據都被回收了。定時器線程是直接調用咱們的函數,若是咱們不主動回收這些數據,那麼就泄露了。咱們能夠看下web.py的WSGI接口實現(在web/application.py文件中):
class application: ... def _cleanup(self): # Threads can be recycled by WSGI servers. # Clearing up all thread-local state to avoid interefereing with subsequent requests. utils.ThreadedDict.clear_all() def wsgifunc(self, *middleware): """Returns a WSGI-compatible function for this application.""" ... def wsgi(env, start_resp): # clear threadlocal to avoid inteference of previous requests self._cleanup() self.load(env) try: # allow uppercase methods only if web.ctx.method.upper() != web.ctx.method: raise web.nomethod() result = self.handle_with_processors() if is_generator(result): result = peep(result) else: result = [result] except web.HTTPError, e: result = [e.data] result = web.safestr(iter(result)) status, headers = web.ctx.status, web.ctx.headers start_resp(status, headers) def cleanup(): self._cleanup() yield '' # force this function to be a generator return itertools.chain(result, cleanup()) for m in middleware: wsgi = m(wsgi) return wsgi
默認的入口函數是class application的wsgifunc()函數的內部函數wsgi(),它第一行就調用了self._cleanup()
。