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 等机制管理。下图对比了两种池化模式下,客户端请求与后端连接的绑定关系,以及会话状态的可保留范围。
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 做简单的读写分离路由。它的适用场景是:连接数非常大、对池化层本身的吞吐和延迟敏感、希望用多核横向扩展池化能力。
选型对照与落地建议
| 维度 | PgBouncer | Pgpool-II | Odyssey |
|---|---|---|---|
| 核心定位 | 纯连接池 | 池化 + 读写分离/故障转移 | 高并发多线程连接池 |
| 进程模型 | 单进程事件驱动 | 多进程 | 多线程 + 协程 |
| 事务池成熟度 | 高 | 中 | 高 |
| 读写分离 | 不支持 | 支持 | 支持(较简单) |
| 运维复杂度 | 低 | 高 | 中 |
| 典型场景 | 短连接收敛、事务池 | 一主多从统一入口 | 超大连接数、多核池化 |
落地时建议先明确三件事:应用是否依赖会话状态(决定能否用事务池)、是否需要读写分离(决定是否引入 Pgpool-II)、池化层自身是否会成为瓶颈(决定是否选 Odyssey)。对大多数“连接数过多”的问题,PgBouncer 的 transaction 模式 + 应用无状态化改造,往往是最短路径。改造后要重点验证:事务外 SET 是否已移入事务、临时表和游标是否已消除、预备语句是否改为 PREPARE 事务内使用或关闭、连接归还后状态是否被 DISCARD ALL 清理干净。只有这些前提成立,事务池的高复用率才不会以“状态串扰”为代价。