12.4 会话状态与连接池陷阱
应用看到的“一个数据库连接”可能依次经过:
当 PgBouncer 采用 transaction pooling 时,client connection 不是 PostgreSQL session 的永久所有者。应用必须把正确性限制在 transaction 边界,不能把上一个 transaction 留下的 session state 当作下一个 transaction 的前提。
12.4.1 session、transaction 与 statement pooling
三种“归还 backend”的时刻
| mode | server connection 何时归还 | 兼容性 | 复用率 |
|---|---|---|---|
| session | client 断开 | 最接近直连 | 最低 |
| transaction | transaction 结束 | app 必须 transaction-aware | 高 |
| statement | 每条 statement 后 | multi-statement transaction 不成立 | 最高、限制最多 |
session pooling 保留一个 client 对一个 backend 的 session 关系,许多 session feature 可用,但吸收连接峰值的能力有限。
transaction pooling:
这非常适合把业务状态装进显式 transaction 的服务,也意味着跨 transaction 的 session assumption 会破坏。
statement pooling 连一个多语句事务都不能正常表达,不适合作为本章业务主线。
Transaction pooling 中哪些东西会坏
PgBouncer 官方 feature map 明确指出 transaction pooling 下不能依赖:
- 普通
SET/RESET的跨事务效果; LISTEN;- SQL-level
PREPARE/DEALLOCATE; WITH HOLDcursor;- preserve/delete rows temp tables;
- session-level advisory locks;
LOAD。
例:
第二个 transaction 不应假定 app.tenant_id 仍存在。更糟的是,拿到的 backend 可能曾服务另一个 client;可靠 pooler 会 reset/track 一部分参数,但应用不能把未知 session residue 当作隔离机制。
一个 transaction 内的状态仍然有意义
transaction pooling 在 BEGIN 到 COMMIT/ROLLBACK 期间固定 backend,所以:
语义成立。关键不是“永远不用 SET”,而是:
且每个 transaction 都重新建立需要的 context。
Autocommit 是 transaction
一条没有显式 BEGIN 的 SQL 在 PostgreSQL 中也运行于一个 transaction;在 transaction pooling 中,statement 完成后 backend 就可能归还。
所以这种代码不成立:
SET LOCAL 必须与受保护查询在同一个显式 transaction object 上执行。
不要用 session advisory lock 做跨请求 ownership
第 10 章已区分 transaction/session advisory lock。transaction pooling 下:
这既会泄漏锁,也会让 unlock 不可达。使用:
pg_advisory_xact_lock,生命周期绑定当前 transaction;- 或把 ownership 建模成持久 lease/row;
- 或为确需 session affinity 的任务使用单独 direct/session-pooled service。
不能为一个特殊 job 把所有在线业务都切到 session pooling;服务职责可以拆分。
12.4.2 预备语句行为必须绑定 PgBouncer 与驱动版本
“Prepared statement 与 PgBouncer 不兼容”过于粗糙
需要至少区分:
PgBouncer 1.21 起可以在 transaction pooling 中跟踪 protocol-level named prepared statements,但必须:
它会在 client/server name 之间重写,并确保目标 backend 已准备该 query。SQL-level PREPARE/DEALLOCATE 仍不受 transaction pooling 支持。
因此不能从“PgBouncer 版本够新”直接推导“所有 driver 默认都安全”。要验证:
pgx 的默认行为
pgx v5.10.0 默认 QueryExecModeCacheStatement:
如果 schema 或 search_path 在缓存后变化,第一次重新执行可能失败,例如 SELECT * 列数变化或 result type 变化。pgx 文档也提示默认 prepared statements 可能与 proxy/PgBouncer 不兼容,建议按环境选择 QueryExecModeExec 或在必要时 simple protocol。
本章保守固定:
该模式:
- 仍使用 extended protocol;
- 不使用 named prepared statement cache;
- 根据 Go argument type 推断 PostgreSQL parameter type;
- 使用 text-formatted parameters/results;
- 单 round trip;
- 比 simple protocol 更优先。
这减少了本章未验证 PgBouncer config 下的一个变量,不代表 statement cache 永远不该用。生产若确认 PgBouncer tracking 与 workload 收益,应单独 A/B 并保存版本/config/DDL 恢复证据。
不要误用 SimpleProtocol
simple protocol 不是“更安全的参数化”。pgx 会在 client 端对参数插值并转义,它适合某些不支持 extended protocol 的 proxy。对标准 PostgreSQL/PgBouncer,优先尝试 QueryExecModeExec。
simple/exec mode 还要求你认真处理 type mapping,尤其 []byte、JSON、用户自定义类型。不要为躲开 prepared statement 问题,悄悄改变参数编码语义而不运行 contract suite。
DDL 后的 cached plan
PgBouncer prepared-statement tracking 提升复用,但若相同 query 的 parameter/result types 在 DDL 后改变,PostgreSQL 可能报:
PgBouncer 文档建议这类 migration 后通过 admin console RECONNECT 让 server connections 重建计划。发布设计应回答:
- DDL 是否改变返回列数/type;
- old/new app 是否使用相同 query text 却期待不同 shape;
- 是否使用
SELECT *; - app pool 是否也有 cache;
- PgBouncer reconnect 如何执行、影响多少连接;
- reconnect storm 与 rollback;
- failure metric 与 smoke query。
不能把 RECONNECT 当作每次 DDL 的盲目万能命令;先证明目标、作用域和版本。
建议的组合矩阵
| pgx mode | PgBouncer transaction pool | 需要验证 |
|---|---|---|
| cache_statement | tracking on | exact versions/config, DDL invalidation |
| cache_statement | tracking off/unknown | 不应默认放行 |
| cache_describe | no named plan | result/arg type drift |
| describe_exec | two round trips | pooler round-trip backend affinity |
| exec | conservative mainline | type mapping, performance |
| simple_protocol | fallback only | client interpolation/type semantics |
“能跑一条 SELECT 1”不能覆盖这个矩阵。本章 promotion blocker 要求用完整下单/支付/DDL smoke 通过实际 primary service。
12.4.3 SET LOCAL、事务边界与 RLS 上下文
SET 与 SET LOCAL
PostgreSQL:
transaction pooling 的主线应是:
set_config(..., true) 的第三个参数表示 transaction-local,便于参数化 value。不要拼:
RLS context 要 fail closed
若 ch23 使用:
policy 要明确 missing context 是:
而不是 fallback 到“全部租户”。还要验证:
- runtime role 不具
BYPASSRLS; - object owner 是否绕过 RLS;
FORCE ROW LEVEL SECURITY是否需要;- SECURITY DEFINER 是否重新建立 context;
- connection reset 后不存在可继承 tenant;
- transaction retry 每次重新
SET LOCAL; - background jobs 使用什么身份。
把 tenant 放到 application_name 不安全也会产生高基数;它是 observability label,不是授权 context。
同一 transaction 才能相信 context
错误:
两个 pool method 可能 acquire 不同连接,也一定是不同 autocommit transaction。正确:
若 query 是单条并且能把 tenant 作为普通 $1 predicate,就优先显式参数;RLS context 用于数据库必须统一执行的访问策略,不是减少一个参数的技巧。
search_path 也不要依赖 session residue
本章所有对象 schema-qualified:
SECURITY DEFINER function 固定:
然后引用 qualified object。这样:
- transaction pooling 不依赖前一 transaction 的 path;
- 恶意同名 object 更难劫持;
- query contract 明确;
- migration 与 app 看同一对象。
若通过 role/database startup parameter 固定 search_path,仍要把它作为 connection contract 验证。
12.4.4 在 ch22、ch23 分别深化池化与权限
本节只建立应用必须知道的最小边界,不在这里展开两个独立大主题。
第 22 章将深入连接治理:
第 23 章将深入安全与访问:
本章保留的 cross-chapter contract:
- 在线服务默认可以在 transaction pooling 下正确运行;
- 正确性不依赖跨 transaction session state;
- runtime role 最小权限、不是 object owner、没有 BYPASSRLS;
- connection/query mode 与 pooler 版本配置绑定;
- 特殊 session workload 使用独立 service,不污染在线主线;
- 权限或 pool 配置变化后重跑同一业务/failure matrix。
本节检查表
- 知道实际 pool_mode,不从端口名猜;
- transaction pooling 下没有跨事务
SET前提; -
LISTEN、temp table、cursor、advisory lock 的 lifetime 已评审; - transaction-local state 与业务 SQL 在同一 tx object;
- pgx 与 PgBouncer exact versions 已记录;
-
max_prepared_statements实际值已记录; - 区分 protocol prepared、SQL PREPARE 与 driver cache;
- query mode 改变后重新验证 type mapping;
- DDL result-shape change 有 cache/reconnect 计划;
-
SET LOCAL或set_config(..., true)fail closed; - tenant context 不是 authorization 的唯一应用侧证据;
- SQL schema-qualified,SECURITY DEFINER path 固定;
- 特殊 session workload 使用独立连接路径。
参考资料
- PgBouncer:SQL feature map
- PgBouncer:max_prepared_statements 与 pool_mode
- PgBouncer FAQ:transaction pooling prepared statements
- pgx v5.10.0:QueryExecMode
- PostgreSQL 18:SET
- Pigsty:PgBouncer Administration
上一节:Go 服务中的连接与事务 · 返回本章目录 · 下一节:服务级可观测性 · 查看全书目录 · 查看索引中心