PostgreSQL 连接数打满怎么办:pgbench 实测直连、每次新建连接与 PgBouncer 事务池
在 PostgreSQL 17.11 + PgBouncer 1.24.1 测试环境、100 万行自造数据上,用 pgbench 实测长连接、每个事务新建连接、经 PgBouncer 转发三种方式的吞吐和建连耗时,复现 remaining connection slots are reserved 报错和 idle in transaction 占满事务池,附只读巡检 SQL 和可复跑脚本。
每次请求新建连接是最贵的用法:实测直连建连约 11.7 毫秒,吞吐从长连接的约 16 万 TPS 掉到 340;而这 11 毫秒几乎都花在 SCRAM 认证上,换成 PgBouncer 并不会自动变快(同样认证方式下 15–17 毫秒)。PgBouncer 真正解决的是「客户端多、服务端连接少」:120 个客户端只占 20 个后端连接、零失败。但只要有人开着事务不提交,事务池照样会被占满。
适用对象
- 长连接是基线:20 个客户端只读压测约 16.0–16.6 万 TPS;改成每个事务新建连接(pgbench -C)后只剩 339–343 TPS,平均建连 11.6–11.7 毫秒。
- 踩坑:我以为接上 PgBouncer 就能把建连变便宜,结果经 PgBouncer(auth_type=scram-sha-256)每次新建连接反而更慢:15.5–17.0 毫秒、235–258 TPS。对照组把 PgBouncer 认证临时改成 trust(仅测试),建连降到 0.26 毫秒、约 1.46–1.49 万 TPS——贵的是 SCRAM 握手,不是 TCP。
- PgBouncer 的价值在连接收敛:直连 120 个客户端时第 78 个连接就报 FATAL: remaining connection slots are reserved…;经 PgBouncer(default_pool_size=20)120 个客户端零失败、约 5.9–6.0 万 TPS,PG 侧只有 20 个后端连接。
- 事务池最怕 idle in transaction:20 个客户端经 PgBouncer 各开一个事务后空闲,第 21 个客户端的 select 1 等了 8.99 秒,直到有事务结束才轮到它。
四种连法,吞吐差了几百倍
A 是应用最理想的状态:20 个长连接一直复用。B 模拟「每个请求开一个连接、用完就关」,这在 PHP、Serverless 函数和没配连接池的脚本里很常见。C、D 分别是经 PgBouncer 的短连接和长连接。
B 的平均延迟 58–59 毫秒里,建连就占了约 11.7 毫秒(pgbench 的 average connection time),剩下的是 20 个客户端排队建连造成的等待;真正执行一条主键查询只要 0.1 毫秒左右。
本节判断
- D 比 A 慢一半多,是因为这个压测的查询极短,PgBouncer 单进程转发本身成了瓶颈。真实业务的查询通常是毫秒级,这层转发开销占比会小得多,但它不是零。
| 方式 | TPS 第 1 / 2 轮 | 平均延迟(毫秒) | 平均建连(毫秒) |
|---|---|---|---|
| A 直连长连接 | 165,610 / 160,085 | 0.121 / 0.125 | — |
| B 直连,每事务新建连接(-C) | 339 / 343 | 58.97 / 58.34 | 11.75 / 11.62 |
| C 经 PgBouncer,每事务新建连接 | 234 / 258 | 85.30 / 77.60 | 17.04 / 15.49 |
| D 经 PgBouncer 长连接 | 64,221 / 68,438 | 0.311 / 0.292 | — |
接了 PgBouncer,短连接反而更慢:贵的是 SCRAM 认证
C 组的结果一开始让我以为配置错了:PgBouncer 已经替我保持着到 PG 的服务端连接,客户端每次新连上来只是和 PgBouncer 握手,为什么比直连还慢?
排查思路是做对照:另起一个 PgBouncer,只把 auth_type 从 scram-sha-256 改成 trust(只在本机测试环境这么做),其余完全一样,再跑同样的 -C。结果建连从 15–17 毫秒降到 0.26 毫秒,TPS 从约 250 升到约 1.46–1.49 万。也就是说,在这台机器上一次新建连接的成本几乎全是 SCRAM-SHA-256 的密码推导(客户端和服务端都要做),而 PgBouncer 是单进程的,20 个客户端同时握手就在它这里排队。
结论很朴素:解决「每次请求都建连」,靠的是让应用复用连接(应用内连接池、长连接),而不是在中间加一层代理。PgBouncer 前面如果还是短连接,认证开销一分不少;生产环境也不要为了快去用 trust,正确做法是让应用侧把连接留住。
| PgBouncer auth_type | TPS | 平均建连(毫秒) | 说明 |
|---|---|---|---|
| scram-sha-256 | 234 / 258 | 17.04 / 15.49 | 与生产一致的配置 |
| trust(仅测试) | 14,927 / 14,628 | 0.258 / 0.264 | 只用于定位开销,生产勿用 |
120 个客户端:直连报错,经 PgBouncer 零失败
把客户端数提到 120,超过 max_connections=100。直连两轮都在第 78 个连接(client 77)失败:FATAL: remaining connection slots are reserved for roles with the SUPERUSER attribute。意思是普通连接位已经用完,只剩 superuser_reserved_connections 留给超级用户的 3 个;连保留位也用完时才会看到 sorry, too many clients already。pgbench 只要有一个连接失败就整体中止,应用侧则表现为部分请求连不上。
为什么是第 78 个而不是第 98 个?这是我自己踩的坑:前一组测试留下的 20 条 PgBouncer 服务端连接还空闲着(server_idle_timeout 默认 600 秒),它们同样占着 PG 的连接位,100 − 3 − 20 = 77。线上也一样,连接池、监控、定时任务的空闲连接都算数,所以先用只读 SQL 第 2 段看清是谁占着连接,再决定要不要调大 max_connections。
经 PgBouncer 再跑 120 个客户端:两轮零失败,约 6.0 万 / 5.9 万 TPS,平均延迟约 2 毫秒(客户端在 PgBouncer 里排队)。压测进行中用只读 SQL 看 PG 侧,pgbench 的后端连接始终是 20 条(idle 19 + active 1 或 idle 20,取决于采样瞬间)。
| 方式 | 客户端 | 结果 | PG 侧后端连接 |
|---|---|---|---|
| 直连 | 120 | 第 78 个连接报 FATAL,pgbench 中止 | 77 + 20(PgBouncer 遗留空闲) |
| 经 PgBouncer(pool 20) | 120 | 0 失败,60,247 / 58,713 TPS | 20 |
事务池的软肋:idle in transaction
transaction 模式下,PgBouncer 只在一个事务结束后才把服务端连接还回池子。我让 20 个客户端经 PgBouncer 各执行 BEGIN 和一条主键查询,然后什么都不做、空闲 12 秒再回滚。3 秒后第 21 个客户端执行最简单的 select 1,两轮分别等了 8.987 秒和 8.999 秒——正好等到前面的事务结束。
这时跑只读 SQL:第 2 段显示 lab / psql / 127.0.0.1 有 20 条连接全部是 idle in transaction;第 4 段逐条列出这 20 个会话,最后一条查询都是 SELECT abalance … ,事务已开约 4 秒。生产上,这通常是代码里开了事务后去调外部接口、或者异常分支没有回滚。
本节判断
- PG 侧给业务账号设 idle_in_transaction_session_timeout(比如 60 秒)可以兜底;PgBouncer 侧可以看 query_wait_timeout(默认 120 秒),排队超过就给客户端报错,而不是无限等。这两个都是改配置,先在测试环境验证。
- transaction 模式还会让会话级状态失效:SET、LISTEN、会话级 advisory lock、WITH HOLD 游标等都可能落到别的服务端连接上。上线前先确认应用没依赖这些。
连接数告警时,我按这个顺序看
从告警到决定要不要上 PgBouncer
- 01
1
先看谁占着连接
只读 SQL 第 1、2 段:总占用比例,以及按用户 / 应用 / 来源地址 / 状态的分布。
- 02
2
区分空闲与事务中空闲
第 3 段看空闲时长分桶,第 4 段看 idle in transaction。前者多是池子配大了,后者是代码问题。
- 03
3
短连接先改应用
如果连接寿命很短、来源是同一批应用,优先让应用复用连接;PgBouncer 解决不了认证开销。
- 04
4
客户端多再上 PgBouncer
实例数 × 每实例池大小 远超 PG 能承受的连接数时,用 PgBouncer transaction 模式收敛;同时设好 idle_in_transaction_session_timeout 和 query_wait_timeout。
参考依据
以下来源用于确认市场趋势、政策背景和术语边界;具体落地方案仍以客户的数据范围、权限和交付目标为准。
常见问题
直接把 max_connections 调到 1000 行不行?
能连上,但每个连接都是一个独立的后端进程,会占内存,连接越多,CPU 调度和锁的竞争也越大。先用只读 SQL 看清连接是谁的、在干什么,通常能砍掉一大半,再决定调大还是加 PgBouncer。修改 max_connections 需要重启。
PgBouncer 选 session 还是 transaction 模式?
session 模式兼容性最好,但一个客户端连接就占一个服务端连接,收敛效果有限;transaction 模式收敛最好,代价是不能依赖会话级状态,并且最怕 idle in transaction。大多数 Web 应用用 transaction 模式,前提是先把会话级用法排查掉。
这些数字能代表生产吗?
不能。这是单机测试环境、自造数据、极短的只读查询,没有网络延迟。绝对值只作参考,结论看的是相对关系:建连成本主要在认证、PgBouncer 收敛连接、idle in transaction 占池。
下一步
推荐动作
你提供只读 SQL 的输出(可脱敏),我们判断连接是被谁占着、该改应用连接池还是上 PgBouncer,以及 max_connections 和超时参数怎么设;生产变更由你方授权执行。