引言
之前分享了一篇《如何提高Django的并发能力》文章,文章的最后结论是采用gunicorn+gthread+django的方式来提高并发能力,该方法简单的说是利用的多线程。
文章也抛出了一个问题:gunicorn+gevent+django+CONN_MAX_AGE会导致数据库连接数飙升,直至占满。如果一定要利用协程的方式启动,该怎么解决这个问题呢?看了一下django源码,找到了问题的根源,写了一下解决办法,下边分享一下。
说明
还是利用上一篇文章《如何提高django的并发能力》的数据模型,这次以get一条数据为例,由于某些原因(好吧手里没有资源),采用了配置稍低的机器:
- 服务器: 4核+4G (docker)
- 压测机: 4核+2G (docker)
- django: 1.8.2
- msyql: 4核+4G(docker) max_connections:1000 max_user_connections:1000
压测方式及命令
- 压测方式: 
- 压测命令:
- ysab: ysab -n 800 -r 10 -u 'localhost://xxx/8080/test'
- 备注: 欢迎使用ysab,(github.com/yunsonbai/ysab)
 
重现问题
settings
| 1 | DATABASES = { | 
启动及压测结果
- 
启动: gunicorn –env DJANGO_SETTINGS_MODULE=test_dj21.settings test_dj21.wsgi:application -w 8 -b 0.0.0.0:8080 -k gevent –max-requests 40960 –max-requests-jitter 5120 
- 
数据库连接数展示  
- 
qps展示  
 为什么能达到1000多, 因为一直再查同一条数据。
问题分析与解决
数据库连接数为什么这么高
| 1 | # django/db/backends/mysql/base.py | 
还有一处诡异的代码
| 1 | class BaseDatabaseWrapper: | 
经过上边的代码,django关于mysql的部分没有使用连接池,导致每次数据库操作都要新建新的连接。更让我有些蒙的是,按照django的文档CONN_MAX_AGE是为了复用连接,但是为什么每次都要新建连接呢?。而且最难受的是一旦我们设置了CONN_MAX_AGE,连接并不会被close掉,而是一直在那占着。
也许是我使用的问题,出现了这个问题。不管如何,最后想了解决办法,请往下看
问题的解决
代码部分
- 
settings代码 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11DATABASES = { 
 'default': {
 'ENGINE': 'test_dj21.db.backends.mysql', # 好吧核心都在这
 'NAME': 'ce',
 'USER': 'root',
 'PASSWORD': '',
 'HOST': '192.168.96.95',
 'PORT': '3306',
 'CONN_MAX_AGE': 600,
 }
 }
- 
test_dj21.db.backends.mysql所在位置  
- 
base.py 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49import random 
 from django.core.exceptions import ImproperlyConfigured
 try:
 import MySQLdb as Database
 except ImportError as err:
 raise ImproperlyConfigured(
 'Error loading MySQLdb module.\n'
 'Did you install mysqlclient?'
 ) from err
 from django.db.backends.mysql.base import *
 from django.db.backends.mysql.base import DatabaseWrapper as _DatabaseWrapper
 class DatabaseWrapper(_DatabaseWrapper):
 def get_new_connection(self, conn_params):
 return ConnectPool.instance(conn_params).get_connection()
 def _close(self):
 return None # 假关闭
 class ConnectPool(object):
 def __init__(self, conn_params):
 self.conn_params = conn_params
 self.n = 5
 self.connects = []
 # 实现单例,实现连接池
 @staticmethod
 def instance(conn_params):
 if not hasattr(ConnectPool, '_instance'):
 ConnectPool._instance = ConnectPool(conn_params)
 return ConnectPool._instance
 def get_connection(self):
 c = None
 if len(self.connects) <= self.n:
 c = Database.connect(**self.conn_params)
 self.connects.append(c)
 if c:
 return c
 index = random.randint(0, self.n)
 try:
 self.connects[index].ping()
 except Exception as e:
 self.connects[index] = Database.connect(**self.conn_params)
 return self.connects[index]
压测结果
- 
数据库连接数展示  
- 
qps展示  
 如果没有self.connects[index].ping()操作压测性能会更好,但是不建议去掉,需要检查连接是否可用。
总结
利用连接池+假关闭的方式解决过高连接数的问题,如果有更好的建议,可以讨论。