这篇讲两件事: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 设小。
  • 开发服务器会为每个请求创建新线程,不需要启用长连接。
  • 若你在连接上修改了隔离级别或时区,最好关闭长连接,或在请求前后恢复默认值。

连接池:为什么需要

连接池的核心是连接复用 + 连接治理

优点:

  1. 减少资源开销:复用连接,减少建连/断连成本。
  2. 统一管理:统一的超时、数量、回收策略,稳定性更好。

典型场景:

  • Gunicorn 多进程 + 协程模型。
  • 短时间内并发暴涨,连接数很容易打满。

举个量级:

2 个服务 × 100 容器 × 10 进程 × 20 协程 = 40,000 连接

MySQL 默认最大连接数通常是 151(不同版本略有差别)。这类场景不加连接池很难撑住。

为什么 Django 不内置连接池

官方社区的典型观点(摘要):

  • 有成熟的第三方实现,Django 不需要重复造轮子。
  • “请求期间持有一个连接”不是真正意义上的池化,效果接近长连接。
  • 与其在应用侧堆连接数,不如先优化缓存与读写分离。

参考讨论:Google Group

Django 连接池方案

最常见的方案是利用 SQLAlchemy 的池化能力,对 Django 的连接逻辑做 patch。

Github 上的轮子:

它们的核心逻辑很一致:

  1. 创建并返回 SQLAlchemy pool
  2. 从 pool 里取连接
  3. 替换 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 足够。
  • 高并发 + 大量协程:连接池更稳。
  • 连接池不是银弹,缓存、读写分离、限流一样重要。