微信公众号

django数据库连接复用

原文连接

引言

之前分享了一篇如何提高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

压测方式及命令

重现问题

settings

1
2
3
4
5
6
7
8
9
10
11
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'ce',
'USER': 'root',
'PASSWORD': '',
'HOST': '192.168.96.95',
'PORT': '3306',
'CONN_MAX_AGE': 600,
}
}

启动及压测结果

  • 启动: 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展示
    qps
    为什么能达到1000多, 因为一直再查同一条数据。

问题分析与解决

数据库连接数为什么这么高

1
2
3
4
5
6
7
8
9
10
# django/db/backends/mysql/base.py
class DatabaseWrapper(BaseDatabaseWrapper):
vendor = 'mysql'
.
.

def get_new_connection(self, conn_params):
c = Database.connect(**conn_params)
print(id(c)) # 好吧我刻意打印了一下这个id, 每次查询都会重新建立连接用新连接操作
return c

还有一处诡异的代码

1
2
3
4
5
6
7
8
9
10
11
12
class BaseDatabaseWrapper:
"""Represent a database connection."""
# Mapping of Field objects to their column types.
data_types = {}
.
.

def _close(self):
if self.connection is not None:
with self.wrap_database_errors:
print('foo close') # 每次查询完又要调用close
return self.connection.close()

经过上边的代码,django关于mysql的部分没有使用连接池,导致每次数据库操作都要新建新的连接。更让我有些蒙的是,按照django的文档CONN_MAX_AGE是为了复用连接,但是为什么每次都要新建连接呢?。而且最难受的是一旦我们设置了CONN_MAX_AGE,连接并不会被close掉,而是一直在那占着。
也许是我使用的问题,出现了这个问题。不管如何,最后想了解决办法,请往下看

问题的解决

代码部分

  • settings代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    DATABASES = {
    '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所在位置
    tree

  • 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
    49
    import 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展示
    qps
    如果没有self.connects[index].ping()操作压测性能会更好,但是不建议去掉,需要检查连接是否可用。

总结

利用连接池+假关闭的方式解决过高连接数的问题,如果有更好的建议,可以讨论。

文章推荐

ysab基于golang的压测工具
yunorm轻量接orm
nginx+tornado折腾笔记

yunsonbai wechat
微信公众号