python常用的web框架,诸如flask,django,在生产部署时,都会选择多进程的部署方式,选用的中间件多为uwsgi或者gunicorn。
如果项目里使用了数据库,那么就要考虑数据库连接在多进程下的一些问题,本文以mysql数据库为例。
1. 多进程下共用数据库连接
python连接mysql的客户端驱动库有很多种,例如pymysql,它们都提供了数据库连接池,连接池是多线程安全的,多进程下并不安全。
多线程的安全,是通过线程锁解决的,这非常容易做到,而多进程加锁则并不容易。
我所谓的多进程,是由主进程fork出来的子进程,如果主进程里创建了数据库连接池,随后fork出子进程,子进程在获取数据库连接时,两个子进程就有可能获得同一个数据库连接,这样就会引发问题。
一个数据库连接,本质上就是一个socket连接,建立socket连接后,得到一个打开的socket对象,当两个进程都用这同一个socket对象发送和接收数据时,就会引发异常。
不论是用uwsgi还是gunicorn,其原理都是相似的,创建出app后,fork子进程,这样做的目的是提供web服务的响应能力,work的数量需要合理配置。
对于这种部署方式,我一直都担心出现子进程共用同一个数据库连接的问题,直到最近,猛然间找到了问题的本质。
2. 避免fork前使用连接
目前所用的数据库连接驱动库,都有一个惰性连接的特性。如果你设置连接池的大小是10,那么当程序启动后,连接池并没有真的被建立,只有当程序进行一次数据库操作时,才会真的去建立连接。当连接数量不够时,才会去新建连接,如果连接池里有空闲的连接,会直接拿来使用。
因此,只要保证在创建出app以后,不使用数据库连接进行任何操作,而是等到有真实的请求到来以后再进行数据库操作即可避免多进程共用数据库连接的问题。
只要父进程没有对数据库进行操作,父进程便不会创建数据库连接池。
当请求真实到达时,已经完成了fork动作,此时的子进程,并没有从父进程那里继承数据库连接池,因为父进程自己也没有创建连接池。
请求打到某个字进程,这个子进程在进行数据库操作时创建只属于自己的数据库连接池,不会受到其他子进程的干扰。
3. 合理设置连接池大小
多进程部署模式下,如果你不开启多线程,那么一个子进程便只有一个线程,此时,你设置连接池的大小为1即可。设置的更大,也不会创建出更多的连接,因为对于单个子进程来说,有一个数据库连接就已经足够了。
为了提高响应能力,你开启线程,uwsgi和gunicorn都允许你这样做。那么你要根据线程的数量来设置连接池的大小,与其相等即可,多了也同样不起作用。