uWSGI定時器致使web.py的內存泄露問題

近期開發了一個小型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的定時器,因此懷疑內存泄露是定時器致使的。

  1. 首先,刪掉定時器後,發現uWSGI進程不會發生內存泄露了。肯定是定時器中的代碼致使的內存泄露。

  2. 而後把定時器中的代碼放到一個請求處理函數中去執行,經過構造HTTP請求來觸發代碼執行。結果是沒有內存泄露。所以,結論是同一段代碼在定時器中執行有內存泄露,在請求處理代碼中執行沒有內存泄露。

  3. 這個實驗也把致使內存泄露的代碼鎖定到了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

經過不斷執行這個命令,咱們發現以下規律:

  1. 若是是在定時器中執行數據庫操做,每次執行都打開數據庫文件一次,可是沒有關閉(上述命令輸出的值在增長)

  2. 若是是請求處理函數中執行數據庫操做,則數據庫文件被打開後會被關閉(上述命令輸出的值不變)

到這邊咱們能夠確認,泄露的是數據庫鏈接對象,並且只有在定時器函數中才會泄露,在請求處理函數中不會

爲啥數據庫鏈接會泄露?

這個問題困擾了我好久。最後採用最笨的辦法去解決 -- 閱讀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.

  1. 該方法目前還沒發現反作用,若是有的話那就是把別人存放的數據也給清除了。

  2. 其餘版本的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()

相關文章
相關標籤/搜索