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.\n'
        '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)

總結

利用連接池解決過高連接數的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM