greentor填坑记
Jun 24 2016经过2周的学习开发,Tornado + Django ORM的环境搭好了,这阶段的学习告一段落,虽然这个环境是一个玩具环境,没有经过生产的检验,但是在搭环境的过程中学习了Tornado,greenlet,Django数据库相关的姿势,感觉还是有不少提升。
在这2天的调试中,暴露出了2个比较严重的问题,记录下填坑过程。
1. 线程安全
WSGI服务器在接受到新的http请求时会开一个新线程来调用application进行处理,Django ORM在有数据库查询的时候,会在当前线程中创建一个新的数据库连接并保存到线程local空间中,在同一个线程中的连接是可以被复用的。不同的线程持有不同的连接,这样就保证Django ORM是线程安全的。
Tornado是单线程的,在Tornado中使用Django ORM无论处理多少请求,都会用同一个保存在当前local()中的连接,这样就必然会产生连接使用的冲突。比如同时并发的2个请求,第一个请求关闭了连接,第二个请求还在继续使用这个连接就会抛出异常。
在greentor的配合下,Tornado涉及数据库连接的请求都运行在greenlet中,如果有一个greenlet local来对每个请求的数据库连接进行隔离,就能避免线程安全问题,在这里的greenlet协程完全可以类比为线程。然而greenlet并没有local,那我们就造一个local出来。
from greenlet import getcurrent
__all__ = ['local']
def _get_local_dict():
current = getcurrent()
s = '_%s__local_dict__' % current.__class__.__name__
if not hasattr(current, s):
setattr(current, s, {})
return getattr(current, s)
class local(object):
def __getattribute__(self, attr):
local_dict = _get_local_dict()
try:
return local_dict[attr]
except KeyError:
raise AttributeError("'local' object has no attribute '%s'" % attr)
def __setattr__(self, attr, value):
local_dict = _get_local_dict()
local_dict[attr] = value
def __delattr__(self, attr):
local_dict = _get_local_dict()
try:
del local_dict[attr]
except KeyError:
raise AttributeError(attr)
实际上就是在当前greenlet对象上绑定一个字典属性用于存储数据,然后就是对Django的连接打补丁了。
from greentor.glocal import local
from django.db.utils import ConnectionHandler as BaseConnectionHandler
class ConnectionHandler(BaseConnectionHandler):
def __init__(self, databases=None):
self._databases = databases
self._connections = local()
import django.db
# 使用greenlet local替换threading local,避免threading safe问题
setattr(django.db, 'connections', ConnectionHandler())
2. 连接自动关闭
signals.request_finished.connect(close_old_connections)
Django db会注册一个信号,在请求处理完成后关闭需要关闭的数据库连接,然而在Tornado的环境中,如果没有手动的关闭数据库连接,这个连接会等到greenlet销毁的时候才会关闭。在使用ab对greentor进行测试时,pymysql会抛出too many connection异常,这是因为request处理完成后没有关闭greenlet中的数据库连接导致的。这就要求类似于Django,必须在每个请求结束的时候关闭当前greenlet的数据库连接。
在Tornado的RequestHandler
提供了on_finish
方法用于重写,on_finish
方法会在finish
方法中被调用,同时要保证finish
方法被运行在greenlet中才能关闭对应的数据库连接。提供一个基类:
import tornado.web
from django.db import connection
class BaseRequestHandler(tornado.web.RequestHandler):
def on_finish(self):
connection.close()
为了保证这个方法运行在子greenlet中,必须在get
,post
等等的方法中调用finish
方法。
这2个问题本来在Django中是不存在的,强行对pymysql异步后,就出现了。更好的解决方案是使用数据库连接池,每个request进来时在池中申请可用的连接,请求结束时释放连接到池中,这样就实现了连接的复用。