美文网首页
gunicorn+gevent+django数据库连接池

gunicorn+gevent+django数据库连接池

作者: yunsonbai | 来源:发表于2019-04-12 18:22 被阅读0次

    原文连接

    引言

    前段时间分享了一篇如何提高django的并发能力文章,文章的最后结论是采用gunicorn+gthread+django的方式来提高并发能力,该方法简单的说是利用的多线程。
    文章也抛出了一个问题:gunicorn+gevent+django+CONN_MAX_AGE会导致数据库连接数飙升,直至占满。如果一定要利用协程的方式启动,该怎么解决这个问题呢?看了一下django源码,找到了问题的根源,写了一下解决办法,下边分享一下。

    说明

    还是利用上一篇文章如何提高django的并发能力的数据模型,这次以get一条数据为例,由于某些原因(好吧手里没有资源),采用了配置稍低的机器:

    • 服务器: 4核+4G (docker)
    • 压测机: 4核+2G (docker)
    • django: 2.0.8
    • msyql: 4核+4G(docker) max_connections:1000 max_user_connections:1000

    压测方式及命令

    重现问题

    settings

    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多, 因为一直再查同一条数据。

    问题分析与解决

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

    # 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
    

    还有一处诡异的代码

    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代码
    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
    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

    总结

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

    文章推荐

    ysab基于golang的压测工具
    yunorm轻量接orm

    相关文章

      网友评论

          本文标题:gunicorn+gevent+django数据库连接池

          本文链接:https://www.haomeiwen.com/subject/akdhwqtx.html