这种增益有一个特定的成本:事务模式默默地破坏了假设从一个请求到下一个请求的稳定 Postgres 连接的一切。会话 SET、LISTEN/NOTIFY、咨询锁、在事务中幸存的游标、命名的预准备语句。本文详细介绍了该机制,列出了这些限制及其确切症状,然后展示了生产中的后端(我们的后端,直接在其存储库中验证)如何配置它而不被它困住。有关此处引用的任何性能数据背后的测量方法,请参阅我们的 基准测试方法。
要点
- 事务模式:服务器连接在每次事务结束时释放,而不是在客户端断开连接时释放。这是共享短 REST API 类型连接的最有效模式。
- 构造上不兼容:会话 SET/RESET、LISTEN/NOTIFY、会话咨询锁、WITH HOLD 游标、从一个请求到另一请求重用的临时表。
- 实践中最常见的陷阱:命名准备语句,默认情况下由多个驱动程序(sqlx、asyncpg、JDBC pgjdbc 驱动程序)激活,可以在不同的服务器连接上重播并在负载下触发类似
prepared statement does not exist的错误。 - 从版本 1.21 开始,PgBouncer 可以在事务模式下遵循协议准备语句(通过服务器连接进行 LRU 缓存)。如果您的应用程序在每个请求上更改
search_path,这不会阻止禁用客户端缓存。 - 在 Aurabase 代码中验证:租户池与
statement_cache_capacity(0)和pool_mode=transaction中的 PgBouncer 一起运行,而 PostgREST 自愿保持直接连接,以便通过 LISTEN/NOTIFY 重新加载其架构。
PgBouncer的3种池化模式
PgBouncer 提供了三种模式,其区别仅在于 Postgres 服务器连接何时返回公共池。官方文档将它们命名为 session、 transaction 和 statement (pgbouncer.org/features.html,“池化模式”部分,2026 年 8 月 24 日访问)。
| 时尚 | 服务器连接松动 | 会话兼容性 |
|---|---|---|
| 会话(默认) | 客户端断开连接时 | 总计:SET、LISTEN、光标,一切都像现场一样工作 |
| 交易 | 在每个事务结束时(COMMIT/ROLLBACK) | 部分:仅保留事务本地的内容 |
| 声明 | 在每个单独的请求之后 | 最小:禁止显式多查询事务 |
会话模式是最宽松的,但在可扩展性方面效率最低:只要客户端保持连接,Postgres 连接就会保留给客户端,即使它在两个请求之间不执行任何操作。语句模式是为非常特殊的情况(只读代理、健康检查)保留的,甚至会破坏经典的显式事务。事务模式是 REST API 实践中占主导地位的折衷方案:每个 HTTP 请求通常对应于单个短 Postgres 事务。
事务模式如何工作,逐个连接
在事务模式下,PgBouncer 仅在客户端打开事务时将服务器连接附加到客户端,并在 COMMIT 或 ROLLBACK 时将其返回到池中。在两个事务之间,同一个客户端可能会发现自己被重新分配到完全不同的服务器连接。
具体来说,当 default_pool_size 为 20 时,PgBouncer 可以同时吸收数百个客户端,这些客户端在任何给定时间实际上只有少数交易正在进行。正是这个比率证明了具有高流量但短事务的 REST API 的事务模式是合理的:稀有资源(Postgres 连接,服务器端内存昂贵)仅在严格必要的时间内被占用。
最后一条评论总结了要点:事务模式之所以有效,是因为它故意破坏了“我的应用程序会话”和“我的 Postgres 连接”之间的链接。基于此链接的所有内容都会中断。下一节将详细列出内容。
交易池模式有何突破
PgBouncer 官方文档明确列出了 PostgreSQL 功能,一旦服务器连接可以在来自同一客户端的两个请求之间回收,这些功能就失去了意义。
| 受影响的功能 | 为什么会坏 | 典型症状 |
|---|---|---|
| 设置/设置会话 | 该设置适用于可以立即回收的连接 | 两个请求之间似乎随机忘记了一个参数 |
| 聆听/通知 | 假设有持续连接来接收通知 | 客户永远不会收到通知,或者只是间歇性地收到通知 |
| 会话咨询锁 | 锁由服务器连接持有,而不是逻辑客户端 | 锁在预期完成之前释放,或者永远不会释放 |
| 带 HOLD 滑块 | 必须在打开它的交易之后继续存在 | 下一次迭代时出现“光标不存在”错误 |
| 临时表 | 与 Postgres 会话相关,与事务无关 | 该表在下一个查询中“消失” |
| 准备好的语句名为 | 在特定服务器连接上准备,在另一个服务器上重播 | 负载下“准备好的语句...不存在” |
大多数这些限制不会在本地开发中体现出来,在本地开发中,单个连接通常服务于所有流量。当多个客户端实际上共享池并且服务器连接实际上在来自同一逻辑客户端的两个请求之间易手时,它们出现在实际负载下。烟雾测试几乎永远不会揭示它们。
准备好的语句:最容易被误解的限制
大多数现代 Postgres 驱动程序默认在协议端准备命名请求,而无需应用程序代码显式请求它。这正是这个陷阱难以预料的原因。
协议准备语句在 Parse时命名并缓存在特定服务器连接上。在事务模式下,可以在来自同一逻辑客户端的两个请求之间将该连接重新分配给另一个客户端。如果驱动程序随后在从未准备好的连接上重播相同的语句名称,则 Postgres 会以显式错误进行响应,对于 sqlx 客户端,通常为 prepared statement "sqlx_s_N" does not exist 。该行为是间歇性的:它取决于连接在负载下的执行情况,而不取决于每次调用时可重现的确定性错误。
无论语言如何,客户端更正都是相同的:对于在事务模式下跨越池化器的任何池,禁用命名准备语句的缓存,或强制未命名查询。在带有 sqlx 的 Rust 中,它通过连接选项上的 statement_cache_capacity(0) 。
从版本 1.21 开始,PgBouncer 缓解了服务器端的部分问题:它可以在事务模式下遵循协议准备好的语句,并在分配的连接上动态准备它们,每个连接都有一个 LRU 缓存,其大小可通过 max_prepared_statements调整。这减少了未命中的次数,但并不能免除您在多租户池中禁用客户端缓存的情况,其中 search_path 从一个请求更改为另一个请求:缓存的计划会在 Parse时冻结已解析表的内部标识符 (OID),并且在另一个架构下重播它可能会从错误的租户返回数据,而不是简单的错误。
Aurabase 如何在事务模式下配置 PgBouncer
Aurabase 存储库将 PgBouncer 部署为共享数据平面 (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) 前面的 pool_mode=transaction ,并在租户的每个专用 Postgres 实例 (deploy/cnpg/tenant-pooler.yaml) 前面部署相同配置的 Pooler CNPG。两条路径都应用上述相同的规则。
源代码记录了这种选择的特定安全原因,而不仅仅是稳定性原因。租户之间共享的 Postgres 池在重用连接上为每个项目放置不同的 search_path 。缓存的准备好的语句会在 Parse时冻结已解析表的 OID;在同一连接上为另一个租户重播它会针对第一个租户的架构运行查询,这是一种隔离绕过,而不仅仅是应用程序错误。 因此,毫无例外地应用 statement_cache_capacity(0),包括在事务模式下也通过 CNPG Pooler 的专用实例上。
第二个会话卫生措施:当每个连接返回池时,挂钩执行 DISCARD ALL (重置设置、在服务器端取消分配准备好的语句、释放咨询锁、清除游标和临时表)。如果没有此钩子,先前请求所造成的会话残留可能会在来自不同租户的下一个请求中泄漏,从而重用相同的回收连接。
Exception accepted: dedicated PostgREST instances remain in direct connection to the primary, without going through the pooler. PostgREST schema reloading relies on LISTEN/NOTIFY, which assumes a persistent connection, exactly the functionality that transaction mode breaks (detail already documented in our article on PostgREST compatibility at Aurabase). RLS settings per request are passed to SET LOCAL within an explicit transaction, the only way to remain compatible with a pool that can change server connections at any COMMIT (see our article onmulti-tenant RLS isolation).
启用事务模式而不破坏您的应用程序
一个简短的清单,适用于在事务模式下从直接 Postgres 连接转移到 PgBouncer 的任何后端。
- 审核应用程序代码。 查找非事务
SET、LISTEN/NOTIFY、会话咨询锁、WITH HOLD游标和查询之间重用的临时表。 - 在显式事务中将会话集替换为本地 集。这是唯一能够在连接回收后正常存在的设置,因为它在 COMMIT/ROLLBACK 时被清除,而不是在下一个连接时泄漏。
- 如果您的池遍历池程序并且模式或角色从一个请求更改为另一请求,则 禁用驱动程序端准备好的语句缓存。性能成本是真实但可衡量的,并且远低于租户之间泄漏的风险。
- 将真正需要 会话模式(迁移、管理脚本、任何依赖于 LISTEN/NOTIFY 的内容)的连接隔离到直接非池化连接,而不是放弃所有其他流量的事务模式。
- 大小
default_pool_size和max_client_conn相对于 Postgres 的实际max_connections,而不是从另一个项目复制的任意数字。 - 在实际负载下进行测试,而不仅仅是冒烟测试。 准备好的语句错误和会话设置泄漏几乎不会出现在单个本地连接上。
- 一旦投入生产,就从 PgBouncer 管理控制台监控
SHOW POOLS和SHOW STATS,以便在客户端可见之前发现池饱和情况。
您应该始终选择事务模式而不是会话模式吗?
不,但它是绝大多数 REST API 的正确默认选择。对于严重依赖于无法快速重构的会话功能的遗留应用程序,或者对于池增益无法补偿迁移工作的低流量,会话模式仍然是首选。
PgBouncer 也不是该池模型的唯一实现:Supavisor (Supabase) 和 PgCat 是最近的两个替代方案,在负载分配和集群方面有不同的权衡。请参阅我们的详细比较 PgBouncer vs Supavisor vs PgCat,根据您的拓扑在三者之间进行选择。
常见问题解答
在生产中激活交易模式后最常出现的问题。