这篇讲两件事:Django 的长连接机制,以及在高并发场景下如何引入连接池。
先把结论说清楚:
- Django 原生支持长连接(
CONN_MAX_AGE),但不内置连接池。 - 连接池适合“高并发 + DB 连接昂贵”的场景。
- 连接池不等于长连接,但二者可以配合。
长连接(Persistent Connections)
长连接的目标是减少“建连/断开”的成本。
Django 用 CONN_MAX_AGE 控制连接最大寿命:
- 默认是
0:每个请求结束时关闭连接。 - 设置为正整数:连接可复用 N 秒。
- 设置为
None:连接永久复用(直到不可用)。
官方文档:Django databases
连接管理机制
- 第一次访问 DB 时,Django 才建立连接。
- 连接超过
CONN_MAX_AGE,或变得不可用,才会被关闭。 - 请求开始时会清理过期连接;请求结束时会清理异常连接。
这意味着:数据库异常通常只影响当前请求,下一个请求会创建新连接。
使用注意
- 每个线程维护一个连接,并发线程越多,连接越多。
- 若大多数请求不访问 DB,长连接意义不大,建议
CONN_MAX_AGE设小。 - 开发服务器会为每个请求创建新线程,不需要启用长连接。
- 若你在连接上修改了隔离级别或时区,最好关闭长连接,或在请求前后恢复默认值。
连接池:为什么需要
连接池的核心是连接复用 + 连接治理。
优点:
- 减少资源开销:复用连接,减少建连/断连成本。
- 统一管理:统一的超时、数量、回收策略,稳定性更好。
典型场景:
- Gunicorn 多进程 + 协程模型。
- 短时间内并发暴涨,连接数很容易打满。
举个量级:
2 个服务 × 100 容器 × 10 进程 × 20 协程 = 40,000 连接
MySQL 默认最大连接数通常是 151(不同版本略有差别)。这类场景不加连接池很难撑住。
为什么 Django 不内置连接池
官方社区的典型观点(摘要):
- 有成熟的第三方实现,Django 不需要重复造轮子。
- “请求期间持有一个连接”不是真正意义上的池化,效果接近长连接。
- 与其在应用侧堆连接数,不如先优化缓存与读写分离。
参考讨论:Google Group
Django 连接池方案
最常见的方案是利用 SQLAlchemy 的池化能力,对 Django 的连接逻辑做 patch。
Github 上的轮子:
它们的核心逻辑很一致:
- 创建并返回 SQLAlchemy pool
- 从 pool 里取连接
- 替换 Django 的
connect()实现
轻量版实现(示例)
新建 db_pool_patch.py:
from django.conf import settings
from sqlalchemy.pool import manage
POOL_PESSIMISTIC_MODE = getattr(settings, "DJ_ORM_POOL_PESSIMISTIC", False)
POOL_SETTINGS = getattr(settings, "DJ_ORM_POOL_OPTIONS", {})
POOL_SETTINGS.setdefault("recycle", 3600)
def is_iterable(value):
try:
iter(value)
return True
except TypeError:
return False
class HashableDict(dict):
def __hash__(self):
items = [(k, tuple(v)) for k, v in self.items() if is_iterable(v)]
return hash(tuple(items))
class ManagerProxy:
def __init__(self, manager):
self.manager = manager
def __getattr__(self, key):
return getattr(self.manager, key)
def connect(self, *args, **kwargs):
if "conv" in kwargs:
kwargs["conv"] = HashableDict(kwargs["conv"])
if "ssl" in kwargs:
kwargs["ssl"] = HashableDict(kwargs["ssl"])
return self.manager.connect(*args, **kwargs)
def patch_mysql():
from django.db.backends.mysql import base as mysql_base
if not hasattr(mysql_base, "_Database"):
mysql_base._Database = mysql_base.Database
manager = manage(mysql_base._Database, **POOL_SETTINGS)
mysql_base.Database = ManagerProxy(manager)
def patch_sqlite3():
from django.db.backends.sqlite3 import base as sqlite3_base
if not hasattr(sqlite3_base, "_Database"):
sqlite3_base._Database = sqlite3_base.Database
sqlite3_base.Database = manage(sqlite3_base._Database, **POOL_SETTINGS)
def install_patch():
patch_mysql()
patch_sqlite3()
为了便于排查连接池状态,可以加监听器:
from django.conf import settings
from sqlalchemy import event, exc
from sqlalchemy.pool import Pool
from log import logger
@event.listens_for(Pool, "checkout")
def _on_checkout(dbapi_connection, connection_record, connection_proxy):
logger.debug("connection retrieved from pool")
if settings.POOL_PESSIMISTIC_MODE:
cursor = dbapi_connection.cursor()
try:
cursor.execute("SELECT 1")
except Exception:
raise exc.DisconnectionError()
finally:
cursor.close()
@event.listens_for(Pool, "checkin")
def _on_checkin(*args, **kwargs):
logger.debug("connection returned to pool")
@event.listens_for(Pool, "connect")
def _on_connect(*args, **kwargs):
logger.debug("connection created")
配置 settings.py:
DJ_ORM_POOL_OPTIONS = {
"pool_size": 20,
"max_overflow": 0,
"recycle": 3600,
}
DJ_ORM_POOL_PESSIMISTIC = True
小结
- 低并发 + 短请求:
CONN_MAX_AGE足够。 - 高并发 + 大量协程:连接池更稳。
- 连接池不是银弹,缓存、读写分离、限流一样重要。