跬步 On Coding

greentor填坑记

经过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中,必须在getpost等等的方法中调用finish方法。

这2个问题本来在Django中是不存在的,强行对pymysql异步后,就出现了。更好的解决方案是使用数据库连接池,每个request进来时在池中申请可用的连接,请求结束时释放连接到池中,这样就实现了连接的复用。