数据库连接池不是越大越好:一次连接耗尽排查与调参
采集服务隔三差五报「连接池耗尽」,重启能恢复,两天后又犯。运维的第一反应是「把 max 调大」——这正是连接池最常见的误区。这篇记录一次完整排查:先算账、再定位、最后才调参。
错误现场
报错很直白:
TimeoutError: connection pool exhausted (timeout=30s)
时间点有规律:每天上午批量任务和报表查询重叠的时候。表面看是「并发太高」,但并发高只是诱因——真正的问题在于连接借出去之后没有及时还。
第一步:先算账,别急着加连接
连接池大小的经典公式(Little's Law):
所需连接数 ≈ QPS × 平均查询耗时
假设 50 QPS、平均查询 20ms:50 × 0.02 = 1。也就是说,理论上一两个连接就够,池子设 10 已经很宽裕。如果 10 个连接都不够用,那基本可以断定不是「数量问题」,而是「占用问题」。
还有一个容易忽略的代价:PostgreSQL 每个连接是一个后端进程,内存开销在 MB 级。连接数不是免费的,max 调大只是把问题推迟。
第二步:看数据库,不只看池
应用侧的指标(active / idle / waiting)能说明现象,但真相在数据库。查当前会话在干什么:
SELECT state, wait_event_type, wait_event, count(*)
FROM pg_stat_activity
WHERE datname = 'serp'
GROUP BY 1, 2, 3
ORDER BY 4 DESC;
重点看三种状态:
- active:正在执行查询——正常
- idle:空闲但连接还在——可以接受(池里本来就该有空闲)
- idle in transaction:事务开着但什么都没干——这是危险信号
这次的结果:十几条连接卡在 idle in transaction,wait_event 是 ClientRead——数据库在等应用发下一条语句,而应用在干别的。
第三步:找到「事务里干别的」的代码
这就是罪魁祸首:一个批次处理写成了这样——
with conn.transaction(): # 事务开始,连接被占用
for keyword in batch:
data = fetch(keyword) # HTTP 请求,几百毫秒
conn.execute("INSERT ...", data)
HTTP 请求被放进了事务里:连接在整个批次期间被占着,什么也没做,就在等网络。批量任务一并发,池子瞬间被占满。
第四步:修复
一、收缩事务边界。 先采集,后写库:
results = [fetch(k) for k in batch] # 事务外做慢操作
with conn.transaction(): # 事务只包写入
for data in results:
conn.execute("INSERT ...", data)
二、确保归还。 异常路径最容易漏。用上下文管理器,别手写 acquire/release:
async with pool.acquire() as conn:
...
三、超时兜底。 借不到连接要快速失败,别无限等;单条 SQL 也要有硬上限:
pool = await asyncpg.create_pool(
dsn,
min_size=2,
max_size=15,
timeout=5, # 借连接最多等 5 秒
command_timeout=30, # 单条 SQL 最多 30 秒
)
第五步:参数怎么定
| 参数 | 建议值 | 理由 |
|---|---|---|
| min | 2 到 5 | 预热,太多纯浪费 |
| max | 10 到 20 | 按公式算,别拍脑袋 |
| acquire timeout | 5s | 借不到快速失败,暴露问题 |
| idle timeout | 5 到 10 分钟 | 回收闲置连接 |
| max lifetime | 30 到 60 分钟 | 防止长连接被中间设备掐断 |
| statement timeout | 10 到 30 秒 | 慢查询硬上限 |
改完跑了一周:连接峰值稳定在 8 左右,批量任务和报表重叠时也在池内消化。
五个坑
坑 1:不够就加 max。 泄漏和长事务不会因为池变大而消失,只是爆炸时间推迟。
坑 2:每个请求新建池或连接。 池的意义就是复用,新建等于没池。
坑 3:不设 max lifetime。 长连接被防火墙或负载均衡悄悄掐断,应用拿到的是死连接,报错还很难懂。
坑 4:事务里做慢操作。 最常见的是把 HTTP 调用放进事务——连接占用的最大放大器。
坑 5:只看池指标不看数据库。 pg_stat_activity 才能告诉你连接在等什么,池的 active/idle 只告诉你多少个。
小结
连接池排查的线:先算账(公式)→ 看数据库状态(idle in transaction)→ 找占用原因(事务边界、泄漏、慢查询)→ 修代码 → 最后才是调参数。参数是最后一步,不是第一步——连接池的问题,九成在代码里,不在配置里。
采集请求的超时与重试设计会直接影响连接占用时长,相关字段与限制见 SerpBase 官方文档。你踩过连接池的什么坑?评论区聊聊。

浙公网安备 33010602011771号