近期开发了一个小型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()
。