最近在折腾一个自己的web异步框架,基于asyncio。出于对机器资源的节省,对高并发的需求,越来越多的人开始转向异步框架,有一些语言在设计之初就以异步为基本核心,比如像js/go,而有些语言是中途转向,增加进来异步功能,比如python/php之类。首先,异步框架只是增加了处理更多连接的能力,即capacity,并没有改进高响应,确切地说反而是增加了latency,也没有改进多cpu的利用能力,确切地说反而降低了对多核的利用能力,通常需要借用supervisor启动多个进程来使用多核。再次,异步框架增加了代码的复杂性,让原本很多容易处理的事情变得复杂,但异步让单核很容易就能管理上千个http连接,这在web的世界里又显得非常重要。本文记录当mysql连接池遇到异步框架时,种种纠结。
在原本的同步时代,一个请求被处理的整个生命周期中独占一个线程,这样只需要在线程启动时,创建一个mysql连接就好了,比如作为全局变量,需要查询数据库时,直接使用这个mysql连接就好,就是这么简单。考虑到分表分库、读写分离,所以一个线程可能就需要创建多个mysql连接,又考虑到怕某个请求出错,连接中会有脏数据,又会设计定期断开重连,又考虑到各种原因导致的反复断开socket、再建立连接、密码授信,会有比较大的开销,所以就出现了代理连接池的设计(db proxy/connection pool)。它带来很多好处,读写分离并自动路由sql query,数据库的使用者不需要关心数据库的主从组织方式;让紧随写库之后的查库能自动落到主库上,弥合掉主从延迟问题;处理请求时,从池子里拿一个可用连接,而不需要关心它是已有的还是新建的,用完还回去就行;最最基本的是,同步时代,一个请求的处理是独占一个mysql连接的,这让事务也非常容易实现。
在异步的时代,所有的事情都在同一个进程里完成,没有多线程,切换的是一个个协程(coroutine),或者说是栈结构和一堆locals变量,这就让连接池这套设计面临很大问题。每个协程没办法独占一个mysql连接,一方面是因为大量的并发会导致大量的协程,mysql连接不能无限创建,另一方面因为是同一个thread,没有线程内全局变量(即pthread_setspecific()和pthread_getspecific()之类的东西)可用,这需要语言解释器有底层的支持,这就是为啥升级到python3.7后,有了contextvar这个库,那早期python版本,就通过传参吧,所有的函数都传一个context参数用以存放协程内共享的数据。但是,有些常见场景,又必须要求独占一个mysql连接,比如事务:
db.begin_transaction()
try:
do_sth()
db.commit()
except:
db.rollback()
假如这里面的do_sth()也有对数据库的操作,那么在该函数里面拿到的db并不是这里的db,即不是同一个mysql连接,这就导致事务无法正确运作。因为连接有状态(stateful connection),需要独占,又因为其他一些原因,又不能独占,所以只能临时独占,用完了得立即还回去,忘记还了,就会导致连接泄露,然后连接还得层层传给每个需要操作数据库的函数,即:
def do_other_thing(db):
...
def do_sth(db):
do_other_thing(db)
async with pool.get_connection() as db:
db.begin_transaction()
try:
do_sth(db)
db.commit()
except:
db.rollback()
注意:这里with ... as ...表示通过context实现自动回收功能。
另一个常见场景,比如我们在存储一篇文章之后,立马又要读取出来返回给客户端(你可能会说,为啥不把存的数据返回,而要再从数据库里读一次,这种情形很常见,比如查询数据的函数里有一堆的数据整理工作,不想拷贝代码,就直接调用,就这么简单的原因),因为是写库,一定落在主库上,而立马读取,如果是同一个连接,聪明点的连接池考虑到主从延迟,会自动将查询也落到主库上,所以这就要求存数据的函数和取数据的函数必须拿到同一个mysql连接,否则就会出现,刚刚写进去的数据立马读取,却不存在,而有些只读请求却意外地落到了主库上,即这里也需要临时独占一个mysql连接。基于上面同样的道理,也需要将写库时使用的连接,层层传递给读取的函数。
函数的调用不复杂还好,如果函数的调用非常复杂,数据库操作非常多,那么层层传递同一个参数就会变得非常让人恶心。有什么办法呢,仔细想想似乎也没有特别好的办法,我想到的大体有以下几种:
1. 上面提到的层层传递是一种不需要任何外界支持的办法
2. 所有的mysql操作函数不直接操作数据库,而只是返回拼接之后的sql,由外层使用同一个mysql连接来依次执行,这个也是不需要外界支持,但如果中间涉及数据整理,就会出现同样的代码要拷贝来拷贝去的问题了
3. 升级到python3.7,使用新版本提供的contextvar来作类似多线程下thread_locals之类的事情,不过,这也涉及几方面的问题,一个是python是Linux的基础部件,一旦升级可能导致整个Linux环境发生混乱,可能需要先升级所使用的Linux发行版本,比如升级ubuntu本身,也需要注意mysql连接的回收问题,跟方法1类似,只是不用层层传参。大量的使用contextvar可能让函数带有状态,可重入性变差。
总之,异步确实让一个简单的事情变得复杂了。目前使用python3.5,还是选择了方案1.