• django 重写 mysql 连接库实现连接池


    django 重写 mysql 连接库实现连接池

    问题

    django 项目使用 gunicorn + gevent 部署,并设置 CONN_MAX_AGE 会导致 mysql 数据库连接数飙升,在高并发模式可能会出现 too many connections 错误。该怎么解决这个问题呢?首先看下 django 源码,找到问题的根源。

    本文 django 版本为 2.2.3。

    问题分析

    首先查看连接部分源码:

    # django/db/backends/mysql/base.py
    
    class DatabaseWrapper(BaseDatabaseWrapper):
        vendor = 'mysql'
    	...
    	...
    	...
        def get_new_connection(self, conn_params):
    	    # 每次查询都会重新建立连接
            return Database.connect(**conn_params)
    	...
    	...
    	...
    

    再查看其基类 BaseDatabaseWrapper

    # django/db/backends/base/base.py
    
    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:
                    # 每次查询完又要调用 close 关闭连接
                    return self.connection.close()
    	...
    	...
    	...
    

    查看源码发现 django 连接 mysql 时没有使用连接池,导致每次数据库操作都要新建新的连接并查询完后关闭,更坑的是按照 django 的官方文档设置 CONN_MAX_AGE 参数是为了复用连接,然后设置了 CONN_MAX_AGE 后,每个新连接查询完后并不会 close 掉,而是一直在那占着。

    问题解决

    通过重写 django 官方 mysql 连接库实现连接池解决。

    settings.py 配置

    
    ...
    DATABASES = {
        'default': {
            'ENGINE': 'db_pool.mysql',     # 重写 mysql 连接库实现连接池
            'NAME': 'devops',
            'USER': 'devops',
            'PASSWORD': 'devops',
            'HOST': '192.168.223.111',
            'PORT': '3306',
            # 'CONN_MAX_AGE': 600,    # 如果使用 db_pool.mysql 绝对不能设置此参数,否则会造成使用连接后不会快速释放到连接池,从而造成连接池阻塞
            # 数据库连接池大小,mysql 总连接数大小为:连接池大小 * 服务进程数
            'DB_POOL_SIZE': 3,     # 默认 5 个
            'OPTIONS': {
                'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
             },
        }
    }
    ...
    

    目录结构

    db_pool/
    ├── __init__.py
    └── mysql
        ├── base.py
        └── __init__.py
    
    • db_pool 位于 django 项目根目录

    base.py

    # -*- coding: utf-8 -*-
    from django.core.exceptions import ImproperlyConfigured
    import queue
    import threading
    
    try:
        import MySQLdb as Database
    except ImportError as err:
        raise ImproperlyConfigured(
            'Error loading MySQLdb module.
    '
            'Did you install mysqlclient?'
        ) from err
    
    from django.db.backends.mysql.base import *
    from django.db.backends.mysql.base import DatabaseWrapper as _DatabaseWrapper
    
    DEFAULT_DB_POOL_SIZE = 5
    
    
    class DatabaseWrapper(_DatabaseWrapper):
        """
        使用此库时绝对不能设置 CONN_MAX_AGE 连接参数,否则会造成使用连接后不会快速释放到连接池,从而造成连接池阻塞
        """
        connect_pools = {}
        pool_size = None
        mutex = threading.Lock()
    
        def get_new_connection(self, conn_params):
            with self.mutex:
                # 获取 DATABASES 配置字典中的 DB_POOL_SIZE 参数
                if not self.pool_size:
                    self.pool_size = self.settings_dict.get('DB_POOL_SIZE') or DEFAULT_DB_POOL_SIZE
                if self.alias not in self.connect_pools:
                    self.connect_pools[self.alias] = ConnectPool(conn_params, self.pool_size)
                return self.connect_pools[self.alias].get_connection()
    
        def _close(self):
            with self.mutex:
                # 覆盖掉原来的 close 方法,查询结束后连接释放回连接池
                if self.connection is not None:
                    with self.wrap_database_errors:
                        return self.connect_pools[self.alias].release_connection(self.connection)
    
    
    class ConnectPool(object):
        def __init__(self, conn_params, pool_size):
            self.conn_params = conn_params
            self.pool_size = pool_size
            self.count = 0
            self.connects = queue.Queue()
    
        def get_connection(self):
            if self.count < self.pool_size:
                self.count = self.count + 1
                return Database.connect(**self.conn_params)
            conn = self.connects.get()
            try:
                # 检测连接是否有效,去掉性能更好,但建议保留
                conn.ping()
            except Exception:
                conn = Database.connect(**self.conn_params)
            return conn
    
        def release_connection(self, conn):
            self.connects.put(conn)
    
    

    总结

    利用连接池解决过高连接数的问题。

  • 相关阅读:
    Mysql的select加锁分析
    浅析Kubernetes的工作原理
    HTTP/2部署使用
    Amazon新一代云端关系数据库Aurora
    为什么 kubernetes 天然适合微服务
    深入解读Service Mesh背后的技术细节
    微服务的接入层设计与动静资源隔离
    Prim算法和Kruskal算法介绍
    DAG及拓扑排序
    BFS和DFS
  • 原文地址:https://www.cnblogs.com/leffss/p/11988183.html
Copyright © 2020-2023  润新知