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()

相关文章
相关标签/搜索