22.3 PgBouncer 池化模式
PgBouncer 的核心价值是把:
复用到:
复用边界越短,利用率通常越高;但 client session 能拥有的 server state 越少。选择 pool mode 本质上是在效率和会话语义之间选合同。
22.3.1 session、transaction、statement pooling
session pooling
特点:
- client session 稳定绑定同一个 PostgreSQL backend;
- 大部分 PostgreSQL session feature 可用;
- server connection 复用发生在客户端 session 之间;
- 大量长连接会长期占住 server slot,即使 idle。
适合:
- 依赖 session state 的旧应用;
LISTEN/NOTIFY;- session advisory lock;
- persistent temp table;
- 无法修改的 driver/tool;
- 需要逐 session 安全上下文且已经审查。
它减少建连 churn,却不一定显著减少同时 backend 数。
transaction pooling
下一个事务可能得到另一个 backend。优点:
- idle client 不占 PostgreSQL backend;
- 短事务 workload 复用率高;
- 可以在 PgBouncer 处排队;
- 应用连接数与 database active concurrency 解耦。
代价:
- arbitrary session state 不能视为 client 私有;
- backend PID 会变化;
- session-scoped feature 可能错误、泄漏或失效;
- driver behavior 必须按协议和版本测试。
Pigsty 默认 pgbouncer_poolmode: transaction,本章 exact 运行也是
transaction mode。
statement pooling
它提供最强复用,也最严格:
- multi-statement transaction 不可作为一般能力;
- transaction-level state 都难以保留;
- 许多应用和 driver 不兼容;
- 显式
BEGIN通常会被禁止。
除非 workload 真正是独立 statement 且通过完整测试,不应只为追求更少连接 就使用。
模式比较
| 能力 | session | transaction | statement |
|---|---|---|---|
| client 稳定绑定 backend | 是 | 事务期间 | 单 statement |
| multi-statement transaction | 是 | 是 | 否/受限 |
| idle client 占 server | 常见 | 否 | 否 |
| arbitrary session SET | 通常可 | 不可依赖 | 不可依赖 |
| session advisory lock | 可 | 不可依赖 | 不可 |
| LISTEN | 可 | 不可依赖 | 不可 |
| persistent temp table | 可 | 风险高 | 不可 |
| server connection 复用 | 低 | 高 | 最高 |
| 应用兼容成本 | 低 | 中/高 | 高 |
具体能力矩阵必须以当前 PgBouncer feature map 为准,不能把表格跨版本永久化。
pool mode 是接口版本
从 session 改 transaction,不是性能参数微调,而是 API breaking change:
需要:
- inventory/配置 diff;
- driver/ORM feature inventory;
- integration test;
- canary;
- pool 与 SQL 双层观察;
- rollback;
- release note。
长事务会抵消事务池
transaction pool 只有在事务短时才有效:
如果应用:
它仍长期独占 backend。优化 pool mode 不能替代缩短事务。
22.3.2 临时表、会话 GUC、监听与咨询锁
会话 GUC:状态跟 backend,不跟 client
危险例子:
transaction pool 中,第二个事务可能:
- 落到另一 backend,没有
tenant_42; - 另一 client 借到第一条 backend,继承
tenant_42; - 与 PgBouncer tracked parameter 行为交互。
本章正式实验强制两个 backend:
两个失败方向都出现:
安全替代:
或:
- fully qualified object name;
- 把 context 作为 SQL 参数;
- 使用 PgBouncer 明确支持/跟踪的 startup parameter;
- 为必须 session state 的工作使用 session/direct endpoint。
SET LOCAL 生命周期被限制在事务内,与 transaction pooling 边界一致。
server_reset_query 不能想当然
本章 PgBouncer 配置:
看到 DISCARD ALL 不能立刻得出“每个 transaction 后一定清理”。具体执行
条件与 pool mode 受 PgBouncer 配置语义约束。本章故意用实验验证,而不是
从配置名推断。
若将 server_reset_query_always=1 作为补救,还要评估:
- 每事务额外成本;
- prepared statement 与 cache;
- extension/session cleanup;
- 是否真正覆盖所有业务状态;
- 当前版本行为。
更安全的原则仍是:不要跨事务依赖未声明的 server session state。
临时表
PostgreSQL temporary table 通常属于 session:
transaction pool 中后续事务可能到另一 backend,表不存在;另一 client 也 可能得到保留该 temp schema 的 backend。
可选策略:
- 在一个显式事务内创建、使用并
ON COMMIT DROP; - 使用普通 staging table + run/tenant key + 权限/清理;
- 选择 session/direct endpoint;
- 把计算改成 CTE、unnest、COPY 到受控表;
- 对 driver/ORM 的隐式 temp table 做集成测试。
即使 ON COMMIT PRESERVE ROWS 在 feature map 中有特定支持描述,也不要把
“某些操作可工作”升级成“temp session semantics 完整保留”。
LISTEN/NOTIFY
LISTEN channel 注册在 PostgreSQL session。transaction pool 释放 backend
后,client 不再稳定拥有那个 registration。
消费者应使用:
- session pooling;
- direct endpoint;
- 专用少量连接;
- reconnect 后重新
LISTEN; - 通知丢失后的 durable catch-up。
NOTIFY 不是持久消息队列。断连与切换时要从表/outbox/offset 补齐。
咨询锁
区分:
事务池中,优先使用 transaction-level lock,并让整个受保护动作处于同一事务。
session lock 的危险:
部署工具常用 session advisory lock 保证单实例迁移;这类工具应走直连管理 端点,不能在没有验证时经过 transaction pool。
cursor、portal 与 COPY
一般原则:
- WITH HOLD cursor 跨事务;
- 某些 ORM server-side cursor;
- streaming result;
- COPY 双向协议;
- replication protocol;
都要按具体 driver/PgBouncer 版本测试。不要只看 SQL 文本。
安全上下文
尤其危险:
若 RLS policy 或函数依赖这些 session GUC,而 transaction pool 没有可靠 设置/清理,可能形成跨租户泄漏。
更安全:
并:
- deny-by-default policy;
- 每事务显式设置;
- missing/invalid context 立即失败;
- 注入 backend reassignment 测试;
- pool 与 security review 联动。
第 23 章会深入该问题。
22.3.3 预备语句支持必须绑定 PgBouncer 与驱动版本
为什么旧结论互相矛盾
常见说法:
另一种新说法:
两句都过度概括。至少要区分:
协议级 prepared statement
PostgreSQL extended query protocol 使用:
现代 PgBouncer 在:
时可以跟踪/重写协议级 prepared statement,并在 client 换 backend 时准备 对应 server statement。
但结论必须绑定:
- PgBouncer version;
max_prepared_statements;- driver version;
- driver prepare threshold/cache;
- query string identity;
- pool mode;
- failover/reconnect;
- ORM query mode。
本章 exact matrix:
实验先在 backend 65171 建立协议 prepared 状态,再占住它,让同一 client
去 65172。所有结果仍正确。这只接受该版本组合。
SQL PREPARE
这些是普通 SQL text。PgBouncer 不按协议 prepared statement 的方式重写 它们。名称只存在于创建它的 PostgreSQL session。
正式实验:
这是正确的负面结果。不能因为协议级测试通过,就允许 SQL PREPARE 跨
transaction。
driver 可能悄悄改变协议
需要检查:
- simple vs extended query;
- auto prepare threshold;
- named vs unnamed statements;
- statement cache size/lifetime;
- pooler compatibility option;
- binary parameter/result;
- multi-statement batch;
- connection reset hook。
升级 driver 或 PgBouncer 后,应把同一 compatibility suite 重跑。版本说明 不能替应用自己的 query shape。
prepared statement 的容量成本
max_prepared_statements 也不是免费开关。PgBouncer 要维护映射,PostgreSQL
backend 要保存 prepared plan。大量唯一 SQL text、动态注释或 query
literal 可能造成:
- mapping/cache 增长;
- server-side prepared statement 增长;
- deallocation churn;
- generic/custom plan 行为变化;
- schema change 后 invalidation。
监控并限制 query shape,参数化而不是把值拼进 SQL。
一个兼容性测试矩阵
输出应写“这个矩阵通过”,而不是“prepared statements 支持”。
22.3.4 池等待、服务时间与背压
SHOW POOLS 是瞬时状态
关键列:
采样一次 cl_waiting=0 不证明没有排队。要:
- 周期采样;
- 导出 Prometheus 指标;
- 记录 acquire/wait histogram;
- 与应用和 PostgreSQL active backend 对齐。
本章两槽实验
配置在第一个 test/test 客户端连接前临时变为:
12 个 client 同时执行:
结果:
最慢请求大约经历 6 个 250 ms 服务批次。它说明 pool 正在做有界排队,不是 性能 SLO;SSH 采样、调度和连接开销也包含在时间里。
queueing latency
当:
短 burst 可以排队后恢复。
当:
队列长度和延迟持续增长。必须:
- 超时;
- 拒绝;
- 降级;
- 限流;
- 减少工作;
- 或增加经过验证的容量。
不能靠无限 max_client_conn 吸收持续过载。
query_wait_timeout
PgBouncer 的 query_wait_timeout 限制 client 等待 server connection 的
时间。超时会断开 client,从而:
- 释放无限排队;
- 给应用一个可观察失败;
- 迫使请求遵守 deadline。
它要小于业务还能接受的剩余 deadline,并与应用 acquire timeout 协调。
过小:
过大:
reserve pool
reserve_pool_size 允许等待超过 reserve_pool_timeout 后额外建立 server
connection。它适合有限 burst headroom,不是永久绕过预算。
要问:
- reserve 乘以多少 database/user pair;
- 多个 PgBouncer instance 的总和;
- PostgreSQL 是否仍有保留槽;
- burst 激活时 CPU/I/O 是否安全;
- reserve 使用是否告警。
背压应向上游传播
一个健康链条:
一个危险链条:
第 22.5 节会把 timeout、breaker 和负载削减放进同一控制面。
恢复配置
实验使用 finally:
恢复失败是 stop condition,不能“继续看看切换会怎样”。生产变更应通过 Pigsty 声明管理,本章 runtime override 只为低噪声教学实验。
本节检查表
参考资料
- PgBouncer:Feature map
- PgBouncer:Configuration
- PgBouncer:Administration console
- PostgreSQL 18:PREPARE
- PostgreSQL 18:SET
上一节:连接的服务端成本 · 返回本章目录 · 下一节:路由与故障切换 · 查看全书目录 · 查看索引中心