PostgreSQL 连接池与 PgBouncer:事务级复用与参数坑
更新时间:2026-09-18。架构与 MVCC 讲过:PostgreSQL 是一连接一进程,连接贵、内存与调度成本高。高并发下与其让数据库直面几千连接,不如前置一个连接复用器。本篇讲清为什么需要池化、PgBouncer 三种模式的语义差别,以及事务级模式最容易踩的「会话状态丢失」坑。应用进程内池化见 连接池调优:HikariCP 参数与连接数公式。
本文要回答的问题
- 为什么 PostgreSQL 特别怕「连接太多」?连接数和内存/CPU 的关系?
- 应用内连接池(HikariCP)和外置 PgBouncer 各自解决什么?
- session / transaction / statement 三种
pool_mode语义差在哪? - 事务级模式下,prepared statement、
SET、临时表、 advisory lock 为什么会「坏」? - pool_size、reserve、库级/用户级覆盖怎么配才合理?
一、连接为什么贵:一连接一进程的代价

每条后端连接的成本:
- 固定内存:进程 +
work_mem(每个排序/哈希节点一份)+ 临时上下文,经验 5~10MB 起步,复杂查询更高; - CPU:连接数远超 CPU 核数时,大量进程争抢 CPU、上下文切换和锁竞争反而让吞吐下降、延迟抖动;
- 即使连接 idle,PostgreSQL 仍要在 ProcArray 等结构里遍历它,连接越多快照/清理开销越大。
经验上限:活跃(正在跑查询的)后端连接数控制在「核数 × 2~4」量级通常最优,再多边际收益为负。问题在于应用实例数 × 每实例池大小很容易膨胀到几百上千——这正是外置池要解决的「连接乘数」。
二、应用内池 vs 外置 PgBouncer
| 方案 | 部署位置 | 解决的问题 | 局限 |
|---|---|---|---|
| HikariCP 等 | 每个应用进程内 | 复用本进程连接、省握手/认证 | 多实例时连接数=实例数×池大小,乘数仍大 |
| PgBouncer | 独立代理(可边车/集中) | 把所有应用的连接收敛成少量后端连接 | 改变会话语义(尤其事务模式)、多一跳 |
二者通常叠加使用:应用内池保留(省本地握手),再统一打到 PgBouncer,由它把后端连接压到数据库能舒适服务的数量。
三、三种 pool_mode:语义与取舍
| 模式 | 后端连接何时释放回池 | 会话状态 | 适用 |
|---|---|---|---|
| session(默认、最稳) | 客户端断开才归还 | 完整保留 | 兼容一切,但复用率低 |
| transaction | 每个事务结束就归还 | 事务结束即清空 | 高并发 OLTP 主力,复用率高 |
| statement | 每条语句结束归还 | 连事务都不能跨语句(不能多语句事务) | 极少用,特殊只读场景 |
事务级复用的关键含义:一个数据库后端连接,在 A 的事务结束后可能立刻被 B 的事务拿去用。因此任何「期望在事务之间黏在同一条物理连接上」的东西都会出问题。
# pgbouncer.ini 核心配置
[databases]
appdb = host=10.0.0.11 port=5432 dbname=appdb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 5000 # 允许多少前端连接进来
default_pool_size = 20 # 每个 (库,用户) 对的后端连接数
reserve_pool_size = 5 # 峰值额外应急连接
reserve_pool_timeout = 3
min_pool_size = 5
server_idle_timeout = 600default_pool_size 是按 (database, user) 组合各算一份,不是全局总连接。多个用户/库会倍增,核算 max_connections 时要按组合数乘。
四、事务模式的四大坑
坑一:prepared statement 默认不能跨事务复用。 应用驱动(JDBC 有 server-prepared、psycopg、Npgsql)若在连接上 PREPARE 后指望下一个事务还在,但物理连接已被别人拿走,会报 prepared statement "xxx" does not exist。解法:
- 新版 PgBouncer(1.21+)用
max_prepared_statements(如 100),代理层缓存/重放 prepared statement; - 旧版要么用 session 模式,要么关闭驱动的 server-side prepare(用客户端模拟参数化)。
坑二:会话级 SET 会泄漏/失效。 SET search_path=...、SET application_name、SET role 在事务结束、连接归还后影响下一个借用者,或被复位。需要会话参数应:
- 放进事务内(
BEGIN; SET LOCAL ...; ...; COMMIT;,SET LOCAL只活在事务里,安全); - 或在 PgBouncer 用
server_reset_query/ 连接参数固定(如把 search_path 绑定到数据库用户/连接串)。
坑三:临时表、LISTEN/NOTIFY、会话 advisory lock 依赖长生命周期会话。 临时表默认事务结束(事务模式正好归还连接)后行为微妙,跨事务用 ON COMMIT PRESERVE ROWS 也只在会话内有效,连接被复用即失效;LISTEN 收不到后续通知;pg_advisory_lock(会话级)会随连接归还被他人「继承/释放」。这类功能要用 session 池或独立直连端口;锁请用事务级 pg_advisory_xact_lock(事务结束自动释放,与事务模式契合)。
坑四:特性协商与会话上下文。 如扩展协议、某些 SET、temp_buffers、自定义 GUC,在事务池里假设「全程不变」会被打破。原则:把所有状态收敛进事务内部,把每个事务设计成自包含。
五、参数怎么配:先算数据库的舒适连接数
数据库可承受后端连接 ≈ min(
max_connections,
CPU核数 × (2~4) + 磁盘并行余量
)
PgBouncer 总后端连接 ≈ default_pool_size × (库×用户组合数) + reserve × 组合数
要保证上面第二行 ≤ 第一行-- 数据库侧
SHOW max_connections; -- 硬上限
SELECT count(*) FROM pg_stat_activity; -- 当前连接
SELECT state, count(*) FROM pg_stat_activity GROUP BY state;
-- 关注 idle in transaction 堆积(它占着后端连接不干活)# pgbouncer 管理控制台(需 admin 用户)
psql -p 6432 -h pgbouncer pgbouncer
SHOW POOLS; -- cl_active/cl_waiting/sv_active/sv_idle,waiting>0 说明池太小
SHOW STATS; -- avg_xact_time、avg_query_time、xact_count
SHOW SERVERS;判读:cl_waiting 长期 > 0、avg_xact_time 高,先查是不是长事务/慢 SQL 占着连接(治本是缩短事务),再考虑加 default_pool_size/reserve;盲目把池调很大只会把压力重新丢回数据库。
六、监控与常见事故
| 现象 | 根因 | 处理 |
|---|---|---|
no more connections allowed | 前端超 max_client_conn | 增前端上限/收敛应用连接 |
prepared statement does not exist | 事务模式 + server prepare | 升 PgBouncer 开 max_prepared_statements 或关闭驱动 prepare |
cl_waiting 高、请求排队 | 池太小或事务太长 | 先缩短事务,再调池 |
| 偶发 search_path 错乱 | 会话 SET 在事务模式泄漏 | 改 SET LOCAL 或固定到用户/连接 |
| idle in transaction 暴涨 | 应用拿到连接后做外部 RPC | 代码把事务范围收紧,配锁等待监控 |
部署坑: PgBouncer 自身也可能成为单点/瓶颈。可用多副本 + LVS/HAProxy/边车模式分摊;它 CPU 很轻,瓶颈通常在后端数据库,不在代理本身。
七、相关与延伸
连接为什么贵见 PostgreSQL 架构与 MVCC;应用内池参数与连接数公式见 连接池调优:HikariCP;
idle in transaction会拖住 VACUUM,见 VACUUM 与表膨胀治理;MySQL 侧连接/线程模型对照见 PostgreSQL 与 MySQL 差异。
总结
PostgreSQL 连接池与 PgBouncer:PG 一连接一进程,活跃后端连接宜控制在核数×2~4,应用多实例的连接乘数要用 PgBouncer 收敛;高并发主力是 pool_mode=transaction(每事务结束归还、复用率最高),但要把所有会话状态收进事务——prepared statement 靠 1.21+ 的 max_prepared_statements、会话参数改 SET LOCAL、临时表/LISTEN/会话 advisory lock 改走 session 池或事务级锁;按 (库,用户) 组合核算 default_pool_size×组合数 ≤ 数据库舒适连接数,用 SHOW POOLS 的 cl_waiting 与 avg_xact_time 调参,先治长事务再扩池。