数据库连接池不是越大越好:一次连接耗尽排查与调参

采集服务隔三差五报「连接池耗尽」,重启能恢复,两天后又犯。运维的第一反应是「把 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 官方文档。你踩过连接池的什么坑?评论区聊聊。

posted @ 2026-10-22 11:46  蜘蛛人  阅读(7)  评论(0)    收藏  举报