数据库连接池(Connection Pooler)是一种用于管理和复用数据库连接的组件,它允许应用程序在需要与数据库建立连接时从连接池中获取连接,而非每次都创建新的连接,以减少连接创建和销毁的开销。今天我们介绍 PostgreSQL 数据库生态中常用的四个具有连接池能力的工具: PgBouncer、Pgpool-II、Odyssey、PgCat。
为什么需要连接池?
通常 DBA 会给 PostgreSQL 实例设置一个合理的 max_connections
值,当连接数超过这个值后会报 FATAL: remaining connection slots are reserved for ...
的错误。
有些人认为 PostgreSQL 不能处理太多连接是因为它的进程模型(为每个连接创建一个 backend 进程)会占用大量内存,但其实更重要的原因是 Snapshot scalability,当有大量连接(即使状态为 idle)时,获取快照信息(GetSnapshotData
)成为了性能瓶颈。
PG 14 针对 Snapshot scalability 的问题进行了优化: Improving Postgres Connection Scalability: Snapshots,降低了 idle 状态连接对数据库性能的影响,因此我们现在可以把 max_connections
设置为一个较大的值(maybe 1000~5000?)。那这是否意味着我们不再需要连接池了?答案是否定的,我能想到的两个原因:
- 应用端大量的短连接频繁创建、删除有性能开销
- 微服务架构下存在大量应用,相对单体架构需要更多的连接,连接数依然可能耗尽
依据连接池运行的位置可简单将其分为两类,一种是存在于应用程序的 driver 里,如 pgjdbc-ng 库提供的 Connection Pool DataSource;另一种是单独部署的进程。后文介绍的四个连接池都归为第二类。
PgBouncer
PgBouncer 是一款轻量的连接池解决方案,单线程、低开销(单个连接占用约 2kB 内存)、使用 libevent 事件驱动库来处理异步 I/O,功能简单,配置容易。
三种使用模式:
- 会话模式(Session pooling): 一个客户连接对应一个服务端连接,支持 PostgreSQL 所有特性,常用于多个数据库的连接映射(Connection Mapping)
- 事务模式(Transaction pooling): 一个服务器连接只在事务期间分配给客户端。当 PgBouncer 察觉到事务结束时,连接将被放回连接池中。这种模式会破坏几个基于会话的功能
- 语句模式(Statement pooling): 不允许多语句事务,意味着 autocommit 必须打开
事务模式不支持的特性有:
- SET/RESET
- LISTEN
- WITH HOLD CURSOR
- PREPARE / DEALLOCATE(不支持直接发送字符串语句,但支持协议层面的 Prepared Statements)
- PRESERVE/DELETE ROWS temp tables
- LOAD statement
- Session-level advisory locks
PgBouncer 1.21 版本将事务模式支持了 Protocol-level Prepared Statements 特性。PR
当一个 PgBouncer 处理能力达到上限时,可以部署多个 PgBouncer。也正是由于 PgBouncer 的单线程轻量模型,让它可以在应用程序没有线程池库的情况下部署在近应用端,提供线程池的能力。一个常见的部署架构如下:
PgBouncer 还可以通过 unix domain socket 将老进程的 fd 迁移到新进程来支持在线重启、在线升级的特性。
Pgpool-II
Pgpool-II 是一个位于应用程序和 PostgreSQL 之间的中间件,旨在提供单机 PG 不具备的特性,如:
- 高可用 —— 通过监视后端 PostgreSQL 实例的可用性,在检测到故障时自动切换到其它可用的实例,实现了自动故障检测和故障转移机制,确保数据库的高可用性和持续服务
- 负载均衡(读写分离) —— 将只读查询分发到多个 PostgreSQL 实例以减轻主库负载,进而获得更高的性能
除此基本功能之外,Pgpool-II 还提供一些其它非常实用的特性:
- 连接池: 管理和维护与 PostgreSQL 数据库的连接池,以减少连接创建和关闭带来的开销
- 在线恢复: 将 crash 的旧主变为 standby 重新加入集群
- Watchdog: 为了解决单个 Pgpool-II 造成的单点故障,Watchdog 协调多个 Pgpool-II 节点构成一个集群,有效避免了单点故障或脑裂的问题
- 查询缓存: In Memory Query Cache 保存 SELECT 语句及其结果的对应关系,相同的语句查询会从缓存返回结果
Pgpool-II 是一个多进程模型:
Parent 作为主进程是所有其它进程的父,它负责派生子进程,child 进程接收并处理来自应用端的连接(每个 child 维护一个私有连接池缓存,一个 database/user 对应的多个连接可以由任意 child 进程处理),worker 进程负责检测流复制延迟,pcp 进程用于管理 Pgpool-II 自身状态,watchdog 进程则负责 Pgpool-II 自身的高可用。
Pgpool-II 可同时处理的连接数为 num_init_children (default 32) * max_pool(default 4),这两个参数的配置需要跟 PostgreSQL 实例的配置相匹配:
max_pool*num_init_children <= (max_connections - superuser_reserved_connections) (no query canceling needed)
max_pool*num_init_children*2 <= (max_connections - superuser_reserved_connections) (query canceling needed)
Pgpool-II 在创建连接的时候会解析服务端发送的 ParameterStatus
,比如 in_hot_standby
可用于判断该实例是否为热备,当解析到应用发来的是只读查询时,可以将该查询分发到 hot standby 节点上。
一个典型的高可用集群架构如下:
Pgpool-II 提供了更多的功能,但配置起来也更加复杂,更容易 shoot yourself in the foot
。由于 Pgpool-II 只有 session 模式,为每个应用端连接维护一个后端连接,它作为 connection pooler 并不能处理并发极高的场景。
Pgpool-II - WHAT, WHY, AND WHERE? 👈🏻 这个视频介绍了 Pgpool-II 最适合的使用场景,也指出了如果只是需要 Connection pooler,Pgpool-II 不是最佳的选择。
Odyssey
Odyssey 是一个由 Yandex 开发的高性能、高可靠的 PostgreSQL 连接池。Odyssey 使用了多线程模型,其比较新颖的地方是它开发了一个 Machinarium 库,可以用同步、过程化的编程方式来编写异步事件驱动程序。
与 PgBouncer 类似,Odyssey 也提供了三种工作模式:
- Session mode (default): 连接在客户端首次查询数据库时建立,并在客户端终止会话之前保持连接
- Transaction mode: 连接在客户端首次查询数据库时建立,并在事务结束时断开连接
- Query mode: 不允许多语句事务,autocommit 必须打开
同样,在事务模式下 Odyssey 也有不支持的特性:
- Temporary tables
- cursors
- advisory locks
- PREPARE
支持 Protocal level Prepared Statement,且早于 PgBouncer。
与 PgBouncer 不同的是,Odyssey 的多线程模式在高负载情况下能够处理更多的请求。同时 Odyssey 在事务结束之后尽可能让来自同一个客户端的事务复用同一个连接。
Odyssey 进程包含如下几个核心模块:
main()
.----------.
| instance |
thread '----------'
.--------. .-------------.
| system | | worker_pool |
'--------' '-------------'
.--------. .---------. .---------. .---------.
| router | | servers | | worker0 | ... | workerN |
'--------' '---------' '---------' '---------'
.---------. .------. thread thread
| console | | cron |
'---------' '------'
各模块的功能可参考 Odyssey architecture and internals 和作者在 CMU ¡Databases! 的介绍 Odyssey: PostgreSQL Connection Proxy!。
PgCat
PgCat 是 PostgresML 开发的一款连接池工具,使用 Rust 语言编写,旨在将 PgBouncer 提升到一个新的水平。PgCat 提供和 PgBouncer 一样的 Session mode 和 Transaction mode,多线程模型,使用 Tokio 异步框架处理请求,能够充分利用多核系统的资源。事务模式同样支持 Protocal level Prepared Statement。
PgCat 支持的特性:
- Query load balancing: 使用可配置的负载均衡策略,将查询均匀地分发到所有可用的副本中
- High availability: PgCat 维护了一个内部映射,用于记录健康和不健康的副本,并且只将流量路由到健康的副本上。这种机制确保只有可靠的副本参与查询处理,从而提高系统的可靠性
- Read/write query separation: 通过解析 SQL 以确定查询的读写意图,并根据需要将查询路由到主库或热备库
- Sharding: 根据配置配置将查询路由到不同的节点,需要跟其它 shard 工具一起使用
有一些公司已经将 PgCat 用在生产环境,比如 PostgreML 和 Instacart。
对比
下表给出了四种工具在连接池特性上的一些对比:
PgBouncer | Pgpool-II | Odyssey | PgCat | |
---|---|---|---|---|
线程模型 | 单线程 | 多进程 | 多线程 | 多线程 |
异步框架 | libevent | Machinarium(epoll) | tokio | |
load balance | 解析同一个域名下的多个 副本来支持负载均衡 | ✅ | ❌ | ✅ |
read/write separation | ❌ | ✅ | ❌ | ✅ |
Pooling modes | session mode, transaction mode, statement mode | session mode only | session mode, transaction mode, query mode | session mode, transaction mode |
单点故障 | 存在 | 可配置 watchdog 消除单点故障 | 存在 | 存在 |
总结
本文介绍了 PostgreSQL 生态常用的四个连接池,其中 PgBouncer 和 Pgpool-II 是两个成熟的工具,PgBouncer 被很多人认为是连接池的事实标准(de facto standard),Pgpool-II 提供了很多额外的特性,反而作为连接池其性能有点差强人意(不支持 transaction mode)。Odyssey、PgCat 可以称作 next generation PostgreSQL pooler 或 modern PostgreSQL pooler,利用多线程和异步事件编程充分发挥计算机的资源,解决 PgBouncer 性能上的不足,并提供一些额外特性。
就我个人而言,我对 Odyssey 的 Machinarium 库和 PgCat 的读写分离非常感兴趣,期待在下一个项目上尽快尝试一下 🧐。