# PgBouncer 池化模式

LLMS 索引： [llms.txt](/llms.txt)

---

PgBouncer 的核心价值是把：

```text
many client connections
```

复用到：

```text
fewer PostgreSQL server connections
```

复用边界越短，利用率通常越高；但 client session 能拥有的 server state
越少。选择 pool mode 本质上是在效率和会话语义之间选合同。

## 22.3.1 session、transaction、statement pooling {#item-22-3-1}

### session pooling

```text
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

```text
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

```text
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：

```text
backend identity changes
session state lifetime changes
prepared behavior changes
cancel path changes
security context risk changes
```

需要：

1. inventory/配置 diff；
2. driver/ORM feature inventory；
3. integration test；
4. canary；
5. pool 与 SQL 双层观察；
6. rollback；
7. release note。

### 长事务会抵消事务池

transaction pool 只有在事务短时才有效：

\[
\text{server pool occupancy}
\approx
\text{arrival rate}
\times
\text{transaction duration}
\]

如果应用：

```text
BEGIN
call remote API
wait user input
stream large response
COMMIT
```

它仍长期独占 backend。优化 pool mode 不能替代缩短事务。

## 22.3.2 临时表、会话 GUC、监听与咨询锁 {#item-22-3-2}

### 会话 GUC：状态跟 backend，不跟 client

危险例子：

```sql
SET search_path = tenant_42, public;
COMMIT;

SELECT * FROM orders;
```

transaction pool 中，第二个事务可能：

- 落到另一 backend，没有 `tenant_42`；
- 另一 client 借到第一条 backend，继承 `tenant_42`；
- 与 PgBouncer tracked parameter 行为交互。

本章正式实验强制两个 backend：

```text
client A, backend 65057: SET search_path=pg_catalog; COMMIT
client B, backend 65057: sees pg_catalog
client A, backend 65058: sees "$user", public
```

两个失败方向都出现：

```text
state loss     A 不能依赖它
state leakage  B 收到它
```

安全替代：

```sql
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 配置：

```text
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：

```sql
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 补齐。

### 咨询锁

区分：

```sql
pg_advisory_lock(...)       -- session level
pg_advisory_xact_lock(...)  -- transaction level
```

事务池中，优先使用 transaction-level lock，并让整个受保护动作处于同一事务。

session lock 的危险：

```text
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

一般原则：

```text
只要协议对象必须跨 transaction 存活，就怀疑 transaction pooling
```

- WITH HOLD cursor 跨事务；
- 某些 ORM server-side cursor；
- streaming result；
- COPY 双向协议；
- replication protocol；

都要按具体 driver/PgBouncer 版本测试。不要只看 SQL 文本。

### 安全上下文

尤其危险：

```sql
SET app.tenant_id = '42';
SET ROLE tenant_role;
```

若 RLS policy 或函数依赖这些 session GUC，而 transaction pool 没有可靠
设置/清理，可能形成跨租户泄漏。

更安全：

```sql
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 与驱动版本 {#item-22-3-3}

### 为什么旧结论互相矛盾

常见说法：

```text
transaction pooling 不支持 prepared statements
```

另一种新说法：

```text
PgBouncer 已支持 prepared statements
```

两句都过度概括。至少要区分：

```text
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 使用：

```text
Parse -> Bind -> Execute
```

现代 PgBouncer 在：

```text
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：

```text
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`

```sql
PREPARE add_one(integer) AS SELECT $1 + 1;
COMMIT;
EXECUTE add_one(41);
```

这些是普通 SQL text。PgBouncer 不按协议 prepared statement 的方式重写
它们。名称只存在于创建它的 PostgreSQL session。

正式实验：

```text
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。

### 一个兼容性测试矩阵

```text
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 池等待、服务时间与背压 {#item-22-3-4}

### `SHOW POOLS` 是瞬时状态

关键列：

```text
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` 客户端连接前临时变为：

```text
default_pool_size=2
reserve_pool_size=0
query_wait_timeout=5
```

12 个 client 同时执行：

```sql
SELECT pg_sleep(0.25), pg_backend_pid();
```

结果：

```text
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

当：

```text
arrival rate < sustainable service rate
```

短 burst 可以排队后恢复。

当：

```text
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 协调。

过小：

```text
健康短 burst 也被拒绝
```

过大：

```text
过期请求占队列
上游已经取消，下游还在等
恢复时形成陈旧洪峰
```

### reserve pool

`reserve_pool_size` 允许等待超过 `reserve_pool_timeout` 后额外建立 server
connection。它适合有限 burst headroom，不是永久绕过预算。

要问：

- reserve 乘以多少 database/user pair；
- 多个 PgBouncer instance 的总和；
- PostgreSQL 是否仍有保留槽；
- burst 激活时 CPU/I/O 是否安全；
- reserve 使用是否告警。

### 背压应向上游传播

一个健康链条：

```text
PgBouncer wait grows
  -> app pool acquire grows
      -> concurrency limiter rejects optional work
          -> HTTP returns retryable overload
              -> client uses bounded jitter/backoff
```

一个危险链条：

```text
pool timeout
  -> immediate retry × N layers
      -> reconnect storm
          -> more auth/backend pressure
              -> longer timeout
```

第 22.5 节会把 timeout、breaker 和负载削减放进同一控制面。

### 恢复配置

实验使用 `finally`：

```text
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 只为低噪声教学实验。

## 本节检查表

```text
[ ] 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](https://www.pgbouncer.org/features.html)
- [PgBouncer：Configuration](https://www.pgbouncer.org/config.html)
- [PgBouncer：Administration console](https://www.pgbouncer.org/usage.html)
- [PostgreSQL 18：PREPARE](https://www.postgresql.org/docs/18/sql-prepare.html)
- [PostgreSQL 18：SET](https://www.postgresql.org/docs/18/sql-set.html)

---

[上一节：连接的服务端成本](../02/) · [返回本章目录](../) · [下一节：路由与故障切换](../04/) ·
[查看全书目录](/toc/) · [查看索引中心](/indexes/)
