22.3 PgBouncer 池化模式
PgBouncer 的核心价值是把:
many client connections复用到:
fewer PostgreSQL server connections复用边界越短,利用率通常越高;但 client session 能拥有的 server state 越少。选择 pool mode 本质上是在效率和会话语义之间选合同。
22.3.1 session、transaction、statement pooling
session pooling
client connects
-> obtains one server connection when needed
-> keeps it until client disconnects特点:
- 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
client transaction begins
-> borrows one server connection
-> transaction ends
-> returns pool下一个事务可能得到另一个 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
one statement
-> one server connection
-> immediately returned它提供最强复用,也最严格:
- 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:
backend identity changes
session state lifetime changes
prepared behavior changes
cancel path changes
security context risk changes需要:
- inventory/配置 diff;
- driver/ORM feature inventory;
- integration test;
- canary;
- pool 与 SQL 双层观察;
- rollback;
- release note。
长事务会抵消事务池
transaction pool 只有在事务短时才有效:
[ \text{server pool occupancy} \approx \text{arrival rate} \times \text{transaction duration} ]
如果应用:
BEGIN
call remote API
wait user input
stream large response
COMMIT它仍长期独占 backend。优化 pool mode 不能替代缩短事务。
22.3.2 临时表、会话 GUC、监听与咨询锁
会话 GUC:状态跟 backend,不跟 client
危险例子:
SET search_path = tenant_42, public;
COMMIT;
SELECT * FROM orders;transaction pool 中,第二个事务可能:
- 落到另一 backend,没有
tenant_42; - 另一 client 借到第一条 backend,继承
tenant_42; - 与 PgBouncer tracked parameter 行为交互。
本章正式实验强制两个 backend:
client A, backend 65057: SET search_path=pg_catalog; COMMIT
client B, backend 65057: sees pg_catalog
client A, backend 65058: sees "$user", public两个失败方向都出现:
state loss A 不能依赖它
state leakage B 收到它安全替代:
BEGIN;
SET LOCAL search_path = tenant_42, public;
SELECT ...;
COMMIT;或:
- fully qualified object name;
- 把 context 作为 SQL 参数;
- 使用 PgBouncer 明确支持/跟踪的 startup parameter;
- 为必须 session state 的工作使用 session/direct endpoint。
SET LOCAL 生命周期被限制在事务内,与 transaction pooling 边界一致。
server_reset_query 不能想当然
本章 PgBouncer 配置:
server_reset_query=DISCARD ALL
server_reset_query_always=0
pool_mode=transaction看到 DISCARD ALL 不能立刻得出“每个 transaction 后一定清理”。具体执行
条件与 pool mode 受 PgBouncer 配置语义约束。本章故意用实验验证,而不是
从配置名推断。
若将 server_reset_query_always=1 作为补救,还要评估:
- 每事务额外成本;
- prepared statement 与 cache;
- extension/session cleanup;
- 是否真正覆盖所有业务状态;
- 当前版本行为。
更安全的原则仍是:不要跨事务依赖未声明的 server session state。
临时表
PostgreSQL temporary table 通常属于 session:
CREATE TEMP TABLE staged (...);
INSERT INTO staged ...;
COMMIT;
SELECT * FROM staged;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 补齐。
咨询锁
区分:
pg_advisory_lock(...) -- session level
pg_advisory_xact_lock(...) -- transaction level事务池中,优先使用 transaction-level lock,并让整个受保护动作处于同一事务。
session lock 的危险:
client A acquires on backend X
transaction ends, X returns pool
client B gets X and inherits lock ownership
client A gets backend Y and cannot reliably unlock X部署工具常用 session advisory lock 保证单实例迁移;这类工具应走直连管理 端点,不能在没有验证时经过 transaction pool。
cursor、portal 与 COPY
一般原则:
只要协议对象必须跨 transaction 存活,就怀疑 transaction pooling- WITH HOLD cursor 跨事务;
- 某些 ORM server-side cursor;
- streaming result;
- COPY 双向协议;
- replication protocol;
都要按具体 driver/PgBouncer 版本测试。不要只看 SQL 文本。
安全上下文
尤其危险:
SET app.tenant_id = '42';
SET ROLE tenant_role;若 RLS policy 或函数依赖这些 session GUC,而 transaction pool 没有可靠 设置/清理,可能形成跨租户泄漏。
更安全:
BEGIN;
SET LOCAL app.tenant_id = '42';
SET LOCAL ROLE tenant_role;
... all protected queries ...
COMMIT;并:
- deny-by-default policy;
- 每事务显式设置;
- missing/invalid context 立即失败;
- 注入 backend reassignment 测试;
- pool 与 security review 联动。
第 23 章会深入该问题。
22.3.3 预备语句支持必须绑定 PgBouncer 与驱动版本
为什么旧结论互相矛盾
常见说法:
transaction pooling 不支持 prepared statements另一种新说法:
PgBouncer 已支持 prepared statements两句都过度概括。至少要区分:
protocol-level named prepared statement
SQL text PREPARE / EXECUTE / DEALLOCATE
unnamed statement
client-side statement cache
driver emulation/simple protocol协议级 prepared statement
PostgreSQL extended query protocol 使用:
Parse -> Bind -> Execute现代 PgBouncer 在:
max_prepared_statements > 0时可以跟踪/重写协议级 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:
PgBouncer 1.25.2
max_prepared_statements 256
psycopg 3.2.9
prepare_threshold 1
pool mode transaction
iterations 12
server backends 2
correct results 12实验先在 backend 65171 建立协议 prepared 状态,再占住它,让同一 client
去 65172。所有结果仍正确。这只接受该版本组合。
SQL PREPARE
PREPARE add_one(integer) AS SELECT $1 + 1;
COMMIT;
EXECUTE add_one(41);这些是普通 SQL text。PgBouncer 不按协议 prepared statement 的方式重写 它们。名称只存在于创建它的 PostgreSQL session。
正式实验:
PREPARE backend 65285
another client holds 65285
EXECUTE backend 65286
result InvalidSqlStatementName
SQLSTATE 26000这是正确的负面结果。不能因为协议级测试通过,就允许 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。
一个兼容性测试矩阵
driver versions current, previous, candidate
pool mode direct, session, transaction
prepare behavior disabled, threshold, forced
query types scalar, array, COPY, cursor, batch
role change reconnect, planned switch
schema change invalidate/reprepare
error cases timeout, cancel, backend close输出应写“这个矩阵通过”,而不是“prepared statements 支持”。
22.3.4 池等待、服务时间与背压
SHOW POOLS 是瞬时状态
关键列:
cl_active 正在使用/等待 server 的 client
cl_waiting 等待分配 server 的 client
sv_active 正在服务 client 的 server connection
sv_idle 可立即借出的 server connection
sv_login 正在建立的 server connection
maxwait 最老等待者等待时间
pool_mode 当前 pool 模式采样一次 cl_waiting=0 不证明没有排队。要:
- 周期采样;
- 导出 Prometheus 指标;
- 记录 acquire/wait histogram;
- 与应用和 PostgreSQL active backend 对齐。
本章两槽实验
配置在第一个 test/test 客户端连接前临时变为:
default_pool_size=2
reserve_pool_size=0
query_wait_timeout=512 个 client 同时执行:
SELECT pg_sleep(0.25), pg_backend_pid();结果:
completed clients 12
unique backend PIDs 2
maximum sv_active 2
maximum cl_waiting 10
minimum duration 254.451 ms
maximum duration 1519.423 ms最慢请求大约经历 6 个 250 ms 服务批次。它说明 pool 正在做有界排队,不是 性能 SLO;SSH 采样、调度和连接开销也包含在时间里。
queueing latency
当:
arrival rate < sustainable service rate短 burst 可以排队后恢复。
当:
arrival rate >= service rate for long enough队列长度和延迟持续增长。必须:
- 超时;
- 拒绝;
- 降级;
- 限流;
- 减少工作;
- 或增加经过验证的容量。
不能靠无限 max_client_conn 吸收持续过载。
query_wait_timeout
PgBouncer 的 query_wait_timeout 限制 client 等待 server connection 的
时间。超时会断开 client,从而:
- 释放无限排队;
- 给应用一个可观察失败;
- 迫使请求遵守 deadline。
它要小于业务还能接受的剩余 deadline,并与应用 acquire timeout 协调。
过小:
健康短 burst 也被拒绝过大:
过期请求占队列
上游已经取消,下游还在等
恢复时形成陈旧洪峰reserve pool
reserve_pool_size 允许等待超过 reserve_pool_timeout 后额外建立 server
connection。它适合有限 burst headroom,不是永久绕过预算。
要问:
- reserve 乘以多少 database/user pair;
- 多个 PgBouncer instance 的总和;
- PostgreSQL 是否仍有保留槽;
- burst 激活时 CPU/I/O 是否安全;
- reserve 使用是否告警。
背压应向上游传播
一个健康链条:
PgBouncer wait grows
-> app pool acquire grows
-> concurrency limiter rejects optional work
-> HTTP returns retryable overload
-> client uses bounded jitter/backoff一个危险链条:
pool timeout
-> immediate retry × N layers
-> reconnect storm
-> more auth/backend pressure
-> longer timeout第 22.5 节会把 timeout、breaker 和负载削减放进同一控制面。
恢复配置
实验使用 finally:
snapshot 50 / 30 / 1 / 120
override 2 / 0 / 1 / 5
run probes
restore 50 / 30 / 1 / 120
verify exact equality
only then allow switchover恢复失败是 stop condition,不能“继续看看切换会怎样”。生产变更应通过 Pigsty 声明管理,本章 runtime override 只为低噪声教学实验。
本节检查表
[ ] pool mode 作为接口版本管理
[ ] 长事务不会长期占满 transaction pool
[ ] session GUC 使用 SET LOCAL 或专用端点
[ ] temp table/LISTEN/advisory lock/cursor 逐项盘点
[ ] RLS/tenant context 做 backend reassignment 测试
[ ] prepared 结论绑定 PgBouncer、driver 和配置版本
[ ] SQL PREPARE 与 protocol prepare 分开测试
[ ] cl_waiting、sv_active、maxwait 有指标
[ ] query_wait_timeout 与 request deadline 对齐
[ ] reserve pool 纳入全局预算
[ ] 配置实验有 exact rollback 和 verification参考资料
- PgBouncer:Feature map
- PgBouncer:Configuration
- PgBouncer:Administration console
- PostgreSQL 18:PREPARE
- PostgreSQL 18:SET
上一节:连接的服务端成本 · 返回本章目录 · 下一节:路由与故障切换 · 查看全书目录 · 查看索引中心