为什么你的 PostgreSQL 不断耗尽连接
Source: Dev.to
理解连接限制
PostgreSQL 强制执行最大并发连接数,由 max_connections 设置决定。典型的默认值为:
- 小型/基础层级: 50–100 个连接
- 通用层级: 100–200 个连接
当应用尝试打开的连接数超过此限制时,PostgreSQL 会返回 TooManyConnectionsError。数据库本身仍然在运行,只是没有空间再接受新连接。
为什么单个连接池不足以解决问题
即使使用了连接池,创建多个池也会快速耗尽限制。
典型应用中的情况
class DatabaseClient:
def __init__(self):
self.pool = create_pool(min=2, max=10) # each instance gets its own pool
如果你的服务生成多个组件,每个组件都实例化 DatabaseClient:
| 组件 | 创建的池数量 | 每个池的连接数 |
|---|---|---|
| 健康检查器 | 1 | 10 |
| 依赖检查器 | 1 | 10 |
| 每个工作者(例如 6) | 6 | 每个 10(共 60) |
| 总计 | 8 | 80 |
运行两个容器会使这个数字翻倍到 160 个连接,轻易超过 100 的 max_connections,从而触发错误。
解决方案:单一共享池
在模块或进程级别创建 一个池,让所有 DatabaseClient 实例共享它。
# shared_pool.py
_shared_pool = None
def get_shared_pool():
global _shared_pool
if _shared_pool is None:
_shared_pool = create_pool(min=2, max=5) # configure as needed
return _shared_pool
# client.py
from shared_pool import get_shared_pool
class DatabaseClient:
def get_connection(self):
return get_shared_pool().acquire()
现在,无论创建多少个 DatabaseClient 对象,它们都共享同一组受限的连接。
需要注意的陷阱
-
close()陷阱
如果客户端的close()方法关闭了共享池,后续的调用将会失败。让单个实例的close()成为空操作,只在整个进程退出时关闭池。 -
健康检查的连锁效应
当数据库耗尽连接时,健康检查查询也会失败。编排系统可能会重启容器,这会创建新池,进一步恶化问题。 -
可配置的池大小
使用环境变量(例如PG_POOL_MAX_SIZE=5)来调整池大小,而无需重新部署。 -
部署前先做简易计算
pool_max_size × max_replicas < max_connections - admin_headroom示例
- 池最大大小:5
- 跨服务的总副本数:10 → 5 × 10 = 50
max_connections:100- 管理员预留空间:20 → 100 − 20 = 80
因为 50 < 80,配置是安全的。如果不满足不等式,请减小池大小或引入专用的连接池工具,如 PgBouncer。
推送更改前的检查清单
- ✅ 在模块/进程级别使用单一共享池。
- ✅ 通过环境变量使池大小可配置。
- ✅ 确保单个实例的
close()不执行任何操作。 - ✅ 仅在进程关闭时关闭共享池。
- ✅ 为所有服务验证连接数计算。
- ✅ 运行语法和单元测试,确保代码正常工作。
TL;DR
- 多个池 = 连接耗尽。
- 单一共享池 = 稳定、可预测的使用。
认识到问题通常是 池太多——而不是没有池——可以把凌晨 1 点的慌乱变成可控的局面。希望这能帮助那些刚收到 “数据库连接耗尽” 警报的同事。 🙂