写给每一个被
too many connections报错折磨过的 Django 开发者。
一、先搞清楚:Django 默认是怎么管连接的?
Django 自带的 django.db.backends 在每次请求结束时,会根据 CONN_MAX_AGE 决定是否关闭数据库连接:
python
# settings.py 默认配置
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'CONN_MAX_AGE': 0, # 每次请求后都关闭连接
}
}
CONN_MAX_AGE = 0:请求结束立刻断开,下个请求重新建连。安全但慢。CONN_MAX_AGE = None:永久复用同一个连接。快但有风险(连接断开后不会自动重连)。CONN_MAX_AGE = 60:60 秒内复用连接,超时后重建。这是官方推荐的折中方案。
但这只是 Django 层面的"连接复用",不是连接池。当并发上来,每个 Worker 进程仍会各自持有连接,连接数 = Worker 数 × 并发请求数,数据库照样扛不住。
二、什么时候你真正需要连接池?
| 场景 | 是否需要连接池 |
|---|---|
| 开发环境,QPS < 10 | ❌ 不需要 |
| 生产环境,Gunicorn/uWSGI 多 Worker,QPS > 50 | ✅ 强烈建议 |
| 使用 Serverless(如 AWS Lambda) | ✅ 需要,否则冷启动+连接数爆炸 |
| 数据库连接数上限较低(如 RDS 默认 100) | ✅ 必须上 |
核心矛盾:应用层想复用连接,但 Django 默认的复用粒度太粗(按进程),不够细(按请求)。
三、四种主流方案,从简单到生产级
方案一:调大 CONN_MAX_AGE(最简单,但不够)
python
DATABASES = {
'default': {
'CONN_MAX_AGE': 600, # 10 分钟内复用
}
}
优点:零依赖,一行代码搞定。
缺点:仍然是进程级复用,没法限制总连接数。适合轻量场景。
方案二:django-db-connection-pool(推荐入门)
这是一个专门为 Django 设计的连接池库,基于 DBUtils.PersistentDB,支持 PostgreSQL / MySQL。
安装:
bash
pip install django-db-connection-pool
配置:
python
DATABASES = {
'default': {
'ENGINE': 'django_db_connection_pool.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': 'mypass',
'HOST': 'localhost',
'POOL_OPTIONS': {
'POOL_SIZE': 10, # 池中最大连接数
'MAX_OVERFLOW': 5, # 超出池大小后最多额外创建的连接数
'RECYCLE': 3600, # 连接最大存活时间(秒)
'TIMEOUT': 30, # 获取连接的等待超时(秒)
},
}
}
关键参数说明:
| 参数 | 含义 | 建议值 |
|---|---|---|
POOL_SIZE |
池中常驻连接数 | Worker 数 × 2 |
MAX_OVERFLOW |
突发时额外允许的连接数 | POOL_SIZE 的 50% |
RECYCLE |
强制回收连接的周期 | 3600(1小时) |
TIMEOUT |
拿不到连接时的等待时间 | 30 秒 |
效果: 原本 10 个 Worker 各自建 10 个连接 = 100 个连接,现在 10 个 Worker 共用 10 个连接的池,峰值最多 15 个。
方案三:PgBouncer(PostgreSQL 生产环境标配)
django-db-connection-pool 是应用层连接池,性能和稳定性不如独立的连接池中间件。PostgreSQL 官方推荐用 PgBouncer。
架构变成:
Django App → PgBouncer (连接池) → PostgreSQL
快速部署(Docker):
yaml
# docker-compose.yml
services:
pgbouncer:
image: edoburu/pgbouncer
environment:
- DATABASE_URL=postgres://user:pass@postgres:5432/mydb
- POOL_MODE=transaction
- MAX_CLIENT_CONN=1000
- DEFAULT_POOL_SIZE=25
ports:
- "6432:6432"
Django 配置(连 PgBouncer,不连 PG 直接):
python
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'HOST': 'pgbouncer', # 注意:不是原来的 pg 主机
'PORT': '6432', # PgBouncer 端口
'NAME': 'mydb',
# ... 其他不变
'CONN_MAX_AGE': 0, # 必须设为 0!因为 PgBouncer 自己管池
}
}
⚠️ 关键坑:CONN_MAX_AGE 必须设为 0。
原因:PgBouncer 的 transaction 模式下,如果 Django 持有连接不放,会导致连接无法回池。设为 0 让 Django 用完即还,PgBouncer 负责复用。
三种 Pool Mode 对比:
| 模式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
session |
支持所有 PG 特性 | 无法复用连接,效果差 | 不推荐 |
transaction |
真正的连接复用,性能最好 | 不支持 Prepared Statement | 大多数 Django 场景推荐 |
statement |
支持 Prepared Statement | 某些驱动兼容问题 | 特殊需求 |
方案四:使用 psycopg2 自带的连接池(轻量替代)
如果你不想引入额外组件,可以用 psycopg2 内置的 ThreadedConnectionPool:
python
# settings.py
import psycopg2
from psycopg2 import pool
# 创建全局连接池(Django 启动时初始化)
connection_pool = psycopg2.pool.ThreadedConnectionPool(
minconn=1,
maxconn=10,
host='localhost',
database='mydb',
user='myuser',
password='mypass'
)
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
# 不配置 HOST/PORT/USER/PASSWORD,用自定义的 getconn
'CONN_MAX_AGE': None,
}
}
# Monkey patch:让 Django 用我们的池
from django.db.backends.postgresql.base import DatabaseWrapper
def get_connection(self):
conn = connection_pool.getconn()
return conn
DatabaseWrapper.get_new_connection = get_connection
# 归还连接
def close_connection(self, conn):
connection_pool.putconn(conn)
self.connection = None
DatabaseWrapper.close = close_connection
优点:无第三方依赖。
缺点:需要 monkey patch,升级 Django 可能失效,不够优雅。
四、怎么选?一张表说清楚
| 方案 | 复杂度 | 性能 | 稳定性 | 推荐场景 |
|---|---|---|---|---|
调 CONN_MAX_AGE |
⭐ | ⭐⭐ | ⭐⭐⭐ | 开发/轻量生产 |
django-db-connection-pool |
⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 中小项目,快速上线 |
| PgBouncer | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | 中大型生产环境 |
| psycopg2 内置池 | ⭐⭐ | ⭐⭐⭐ | ⭐⭐ | 不想加依赖的小项目 |
五、生产环境的完整配置示例(推荐组合)
python
# settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'mydb',
'USER': 'myuser',
'PASSWORD': env('DB_PASSWORD'),
'HOST': 'pgbouncer', # 指向 PgBouncer
'PORT': '6432',
'CONN_MAX_AGE': 0, # ⚠️ 关键:必须为 0
'OPTIONS': {
'connect_timeout': 10,
'application_name': 'myapp', # 方便 PG 侧监控
},
}
}
# Gunicorn 配置(配合连接池)
# gunicorn.conf.py
workers = 4 # Worker 数 = POOL_SIZE / 2 左右
worker_class = 'gevent' # 或 sync,取决于你的视图是否有阻塞 IO
timeout = 30
keepalive = 5
六、监控:你怎么知道连接池生效了?
sql
-- PostgreSQL 查看当前连接数
SELECT count(*), state
FROM pg_stat_activity
GROUP BY state;
-- 查看 PgBouncer 池状态(连接 PgBouncer 的管理库)
SHOW POOLS;
SHOW CLIENTS;
关键指标:
active connections应远小于max_connectionswaiting requests应接近 0- 如果
waiting requests持续 > 0,说明池太小,需要调大POOL_SIZE
总结
| 核心观点 | 说明 |
|---|---|
CONN_MAX_AGE 不是连接池 |
只是进程级复用,不够用 |
中小项目先上 django-db-connection-pool |
一行 ENGINE 换掉,零学习成本 |
| 生产环境请上 PgBouncer | 稳定、可观测、官方推荐 |
用 PgBouncer 时 CONN_MAX_AGE 必须为 0 |
否则连接无法回池,池白搭 |
| 池大小 ≈ Worker 数 × 2 | 留 50% 余量应对突发 |
连接池不是银弹,但它是 Django 从"能跑"到"扛得住"的必经之路。