跬步 On Coding

Django db使用MySQL连接池

Django db模块本身不支持MySQL连接池,只有一个配置CONN_MAX_AGE连接最大存活时间,如果WSGI服务器使用了线程池技术,会达到连接复用的效果。但是如果WSGI服务如果是每个请求都创建新的线程,那么这个配置没有任何效果,因为连接保存在Thread.local()名称空间中,在不同的线程中不能复用。

在上一篇greentor MySQL连接池实现中已经实现了MySQL连接池,只需要重写Django MySQL backend以支持连接池,就能达到连接复用的目的,减少socket 3次握手的开销,提高性能。

https://github.com/zhu327/greentor/blob/master/demo/core/base.py

greentor MySQL连接池实现

https://en.wikipedia.org/wiki/Connection_pool

通过greentor实现了pymysql在Tornado上异步调用的过程后发现,每次建立数据库连接都会经过socket 3次握手,而每一次socket读写都会伴随着greenlet的切换,以及ioloop的callback过程,虽然是异步了,但是IO性能并没有提升,所以在研究了TorMySQL连接池的实现后,实现了greentor自己的连接池。

https://github.com/zhu327/greentor/blob/master/greentor/green.py

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出来。

greentor Tornado异步方案

https://emptysqua.re/blog/motor-internals-how-i-asynchronized-a-synchronous-library/

这篇文章是Motor的作者介绍Motor如何通过Greenlet来实现PyMongo在Tornado中异步调用的原理,总结来说就一下几点。

  1. 使用Torando的IOStream包装socket以实现异步调度
  2. 把IOStream的读写操作放在greenlet中运行,并注册一个switch到当前greenlet的callback到IOStream的Futrue中
  3. 在发生读写操作是switch到当前greenlet的父greenlet继续执行,挂起当前greenlet
  4. 在IOStream的读写操作完成后调用callback switch到挂起的子greenlet中继续执行
def tornado_motor_sock_method(method):
    coro = gen.coroutine(method)

    @functools.wraps(method)
    def wrapped(self, *args, **kwargs):
        #当前greenlet是一个子greenlet
        child_gr = greenlet.getcurrent()
        #获取当前greenlet的父greenlet,即之前代码提到过的asynchronize所在的greenlet
        main = child_gr.parent

        def callback(future):
            if future.exc_info():
                child_gr.throw(*future.exc_info())
            elif future.exception():
                child_gr.throw(future.exception())
            else:
                #当future的结果到达,切换回挂起的子greenlet
                child_gr.switch(future.result())

        #保证callback在当前greenlet的父greenlet中运行
        self.io_loop.add_future(coro(self, *args, **kwargs), callback)
        #return这句会暂时挂起当前greenlet,将控制权切换回父greenlet,
        #在上面的callback执行时,才会切换回当前greenlet,return语句返回
        return main.switch()
    return wrapped

使用greenlet的好处是我们可以通过这个挂起,唤醒的过程来中断当前的同步代码,而不需要用Tornado自己实现协程,每次都要yield出来,然后回调。通过使用greenlet可以很方便的把同步的网络IO库修改为支持Tornado的异步库。

Python协程

https://zh.wikipedia.org/zh-cn/%E5%8D%8F%E7%A8%8B

协程可以理解为线程中的微线程,通过手动挂起函数的执行状态,在合适的时机再次激活继续运行,而不需要上下文切换。所以在python中使用协程会比线程性能更好。

Tornado协程

http://blog.csdn.net/wyx819/article/details/45420017

上面有大牛分析的Tornado的线程实现,依赖与Tornado的IOLoop,所以不能单独拿出来使用。有几个需要理解的概念:

  1. Future对象 用来保存异步获取到的结果,并在set_reslut的时候调用callback方法,把对应的callback方法放到ioloop的callback列表中等待下一次ioloop循环再执行
  2. 装饰器coroutine 在这个装饰器中实现了协程的调度,通过不断的调用next函数来不断获取Future对象,然后每次拿到Future对象在add_callback到ioloop上,等到Future被set_reslut后再次next,直到生成器中抛出Return的异常。

具体的实现过程不是很好描述,调度过程比较复杂,还是看看参考文章大牛的解析吧。