4. 连接池核心原理与选型

PostgreSQL 采用进程模型:每建立一个客户端连接,后端就会 fork 出一个独立的后端进程,并为其分配约几 MB 的私有内存(典型 shared_buffers 之外的 per-backend 开销在 5~10 MB 量级,取决于 work_mem、temp_buffers 等会话级参数)。这意味着连接不是“免费”的:连接建立需要 TCP 握手、认证、初始化 catalog 缓存;连接保持需要常驻内存;连接切换需要进程调度。当应用侧出现成百上千个短连接、或大量空闲连接时,数据库会先被连接数压垮,而不是被 SQL 压垮。

连接池要解决的核心矛盾是:应用希望“随时有连接可用”,数据库希望“同时活跃的连接尽量少”。池化通过复用少量真实后端连接来承载大量逻辑客户端,把“连接建立/销毁”的成本摊薄,并把并发压力限制在数据库可承受的范围内。理解连接池,关键是理解它复用的粒度——是复用整个会话,还是只复用事务。

会话池与事务池:复用粒度的本质差异

会话池(session pooling)中,一个客户端在连接期间独占一个后端连接,直到客户端断开才归还。它保留了完整的会话状态:会话级 GUC 参数、SET 变量、临时表、预备语句、游标、LISTEN/NOTIFY、advisory lock 等都能跨语句保持。代价是复用率低——客户端空闲时后端连接也被占用,无法给其他客户端使用。

事务池(transaction pooling)中,后端连接只在一个事务期间绑定给客户端,事务提交或回滚后立即归还。复用率极高,少量后端连接即可支撑大量客户端。代价是会话状态不能跨事务保留:事务外的 SET、临时表、会话级预备语句、游标、LISTEN 都可能失效或串到别的客户端。因此事务池要求应用“无状态化”:每个事务自包含,会话级设置要么放进事务内,要么通过连接参数在建立时统一注入。

事务池下,SET search_path、SET statement_timeout 等若在事务外执行,可能在下一次借出连接时被其他客户端看到或丢失。正确做法是放进事务内,或使用 PgBouncer 的 track_extra_parameters、server_reset_query 等机制管理。

下图对比了两种池化模式下,客户端请求与后端连接的绑定关系,以及会话状态的可保留范围。

会话池 vs 事务池:连接绑定粒度与状态保留 会话池 Session Pooling 客户端 A 客户端 B 客户端 C 后端 1 后端 2 后端 3 1 客户端 ↔ 1 后端,独占至断开 会话状态完整保留 空闲连接占用后端,复用率低 适合:长连接、会话状态依赖强 事务池 Transaction Pooling 客户端 A 客户端 B 客户端 C 后端 1 后端 2 事务期间绑定,提交即归还 会话状态不跨事务保留 复用率高,后端连接数可控 要求应用无状态化

PgBouncer:轻量、专注事务池

PgBouncer 是单进程、事件驱动的轻量连接池,核心定位就是“把大量客户端连接收敛到少量 PostgreSQL 后端连接”。它支持 session、transaction、statement 三种池模式,生产上最常用 transaction。关键参数包括:pool_mode、max_client_conn(允许的客户端连接上限)、default_pool_size(每个 user/database 对的后端连接数)、reserve_pool_size(突发时额外借出的连接数)。

[databases]
appdb = host=10.0.0.11 port=5432 dbname=appdb

[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 40
reserve_pool_size = 10
server_reset_query = DISCARD ALL
track_extra_parameters = search_path,statement_timeout

上面示例的含义是:应用连到 6432,最多 2000 个客户端连接,但真正打到 PostgreSQL 的后端连接默认只有 40 个,突发时最多再借 10 个。事务结束时执行 DISCARD ALL 清理会话状态,并把 search_path、statement_timeout 纳入跟踪,避免跨客户端串扰。PgBouncer 的优势是内存占用小、部署简单、事务池成熟;局限是它只做池化,不做读写分离、不解析 SQL、不提供查询缓存。

Pgpool-II:池化 + 中间件能力

Pgpool-II 不只是连接池,它同时提供读写分离、负载均衡、自动故障转移、并行查询、连接池等能力。它的连接池同样支持 session 和 transaction 模式,但更常被用于“一主多从”的读写分离架构:写请求路由到主库,读请求按权重分发到从库。代价是它比 PgBouncer 重,需要理解 backend_clustering_mode、load_balance_mode、health_check 等配置,且读写分离依赖 SQL 解析,对复杂语句、函数内写操作、临时表等场景需要谨慎设置 statement_level_load_balance 和 black_query_pattern_list。

选型上可以这样判断:如果只需要“把连接数压下来”,PgBouncer 更简单、更稳;如果还需要读写分离、故障切换、多节点统一入口,Pgpool-II 更合适,但要接受更高的运维复杂度。

Odyssey:多线程、面向高并发

Odyssey 是 Yandex 开源的连接池,采用多线程 + 协程模型,相比 PgBouncer 的单进程事件循环,更能利用多核 CPU,在高并发连接场景下延迟和吞吐表现更好。它支持 transaction、session、pipeline 等池模式,配置以 odyssey.conf 为主,典型参数有 pool_size、pool_timeout、client_max、workers。Odyssey 还支持通过 storage 和 routing 做简单的读写分离路由。它的适用场景是:连接数非常大、对池化层本身的吞吐和延迟敏感、希望用多核横向扩展池化能力。

选型对照与落地建议

维度PgBouncerPgpool-IIOdyssey
核心定位纯连接池池化 + 读写分离/故障转移高并发多线程连接池
进程模型单进程事件驱动多进程多线程 + 协程
事务池成熟度高中高
读写分离不支持支持支持(较简单)
运维复杂度低高中
典型场景短连接收敛、事务池一主多从统一入口超大连接数、多核池化

落地时建议先明确三件事:应用是否依赖会话状态(决定能否用事务池)、是否需要读写分离(决定是否引入 Pgpool-II)、池化层自身是否会成为瓶颈(决定是否选 Odyssey)。对大多数“连接数过多”的问题,PgBouncer 的 transaction 模式 + 应用无状态化改造,往往是最短路径。改造后要重点验证:事务外 SET 是否已移入事务、临时表和游标是否已消除、预备语句是否改为 PREPARE 事务内使用或关闭、连接归还后状态是否被 DISCARD ALL 清理干净。只有这些前提成立,事务池的高复用率才不会以“状态串扰”为代价。