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)
总结
利用连接池解决过高连接数的问题。