← 返回信息流
AI 资讯Hacker News·1 小时前

我们将 PgBouncer 吞吐量提升了 4 倍

原标题:We scaled PgBouncer to 4x throughput

速览

我们通过对 PgBouncer 进行深度优化,成功将其吞吐量提升至原来的四倍。这一改进显著提高了数据库连接池的性能和效率,对依赖 PostgreSQL 的应用系统具有积极影响。该成果展示了在基础架构层面进行性能调优的潜力。

AI 深度解读

背景

PgBouncer 是一个轻量级的 PostgreSQL 连接池中间件,广泛用于管理数据库连接,减少 Postgres 的连接开销。然而,PgBouncer 是单线程的:无论机器有多少个 CPU 核心,它都只使用一个核心。在一台 16 vCPU 的机器上,这意味着一个核心负责所有连接池工作,而其他 15 个核心闲置。当连接池成为瓶颈时,吞吐量早在 Postgres 自身达到极限之前就被封顶了。在 ClickHouse Managed Postgres 中,团队需要解决这个单线程限制,让 PgBouncer 能够充分利用多核机器的性能。

核心内容

ClickHouse Managed Postgres 团队采用了一种多进程方案来扩展 PgBouncer:他们运行一组 PgBouncer 进程,进程数量与机器可用的 CPU 核心数成比例。每个进程都绑定同一个端口,并启用 so_reuseport 选项。内核负责将传入的连接请求负载均衡到各个进程。因此,客户端仍然只需连接到一个端点,并不知道背后有多个 PgBouncer 进程。这正是 PgBouncer 官方文档所建议的利用多核的方法:每个进程是单线程的,而 so_reuseport 让所有核心都工作起来。

但有一个关键问题:查询取消(Query Cancellation)。Postgres 的取消请求通过一条全新的连接发送,携带一个取消密钥,与执行查询的连接是分开的。启用 so_reuseport 后,内核可能将这条新连接分配给一个不同于持有会话的进程。取消请求落到一个从未听说过该查询的进程上,导致取消无效。解决办法是“Peering”(进程间感知)。所有进程彼此知晓,因此如果一个取消请求落到了错误的进程,该进程会将其转发给实际拥有该会话的进程。这样,即使任意请求可能到达任意进程,取消操作在整个进程组中都能正常工作。

连接池以事务模式运行,因此一个事务提交后,服务器连接立即返回池中。连接预算在进程组之间分配:max_client_connmax_db_connections 被除以进程数,从而整个进程组不会超过 Postgres 的承载上限。

团队在真实硬件上进行了测试。使用相同的 AWS EC2 实例:一台 16 vCPU 的 c7i.4xlarge 作为连接池机器,另一台运行 Postgres,第三台使用 pgbench 在只读、事务池模式下驱动负载。一组机器运行单个 PgBouncer 进程,另一组运行 16 个进程。相同实例类型、相同 Postgres、相同工作负载,唯一变量是进程数。

他们从 8 个客户端连接逐步增加到 256 个,测量吞吐量和 CPU 利用率。单个进程在约 87k 事务/秒达到峰值,随后在更多负载下性能下降,256 个客户端时降至 77k 事务/秒,因为所有请求争抢一个核心。而 16 个进程的组持续增长,达到约 336k 事务/秒,大约是 4 倍,因为它有更多核心可用。

CPU 利用率方面:单个进程在负载下 pidstat 显示 PgBouncer 进程 CPU 占用约 97%,占满一个核心,而整台 16 vCPU 机器利用率低于 10%。进程组则分散在机器上,达到约 8 个核心忙碌,并且当 Postgres 和负载生成器成为瓶颈时仍有剩余空间。在 256 个客户端稳定负载下,单进程机器整机 CPU 约 9%,而进程组约 52%。相同实例、相同 Postgres、相同工作负载,一种配置让机器闲置,另一种则让它工作。EC2 的 CloudWatch 指标也从外部证实了这一点:单进程实例平均 CPUUtilization 约 16%,进程组约 60%。虽然 CloudWatch 读数略高于虚拟机内部,但差距一致:在您为 16 vCPU 付费的机器上,单个 PgBouncer 几乎浪费了所有核心。

连接上限也类似:单个进程自己执行 max_client_conn,一旦超过,新客户端会被拒绝,返回错误 FATAL: no more connections allowed (max_client_conn)。将预算分摊到进程组,就可以提高总上限,同时每个进程和 Postgres 自身保持在安全范围内。

在少量连接时,单个进程实际上表现良好,甚至略快,因为没有什么可并行化,进程组的连接被分散得很薄。差距恰恰在真正需要的地方出现:在真实并发场景下,一个核心成为瓶颈。

关键要点

  • PgBouncer 是单线程的,每个进程只使用一个 CPU 核心,无论机器有多少核心。
  • 通过 so_reuseport 让多个 PgBouncer 进程绑定同一端口,内核自动负载均衡,实现多核利用。
  • 查询取消问题通过 Peering(进程间转发)解决:取消请求如果落到错误进程,会被转发给持有会话的进程。
  • 连接池以事务模式运行,max_client_connmax_db_connections 在进程间均分,确保整体不超过 Postgres 限制。
  • 实测对比:16 vCPU 实例上,16 个进程的组吞吐量可达约 336k 事务/秒,是单个进程(约 87k 峰值)的约 4 倍。
  • 单个进程在满载时只用 1 个核心(约 97% CPU),整机利用率低于 10%;进程组可占用约 8 个核心,整机利用率约 50-60%,更高效利用付费资源。
  • 少量连接时单进程略有优势,但高并发下多进程方案优势明显,连接池不再成为瓶颈。
  • 该方案是 ClickHouse Managed Postgres 的默认配置,开箱即用。

意义与影响

这项实践展示了如何通过简单的系统级技术(so_reuseport 和进程间 Peering)突破单线程连接池的瓶颈,让 PgBouncer 在多核机器上发挥出应有的性能。对于使用 PostgreSQL 的应用,尤其是需要处理大量并发连接的场景,连接池往往是容易被忽视的瓶颈。ClickHouse 团队的方法提供了一种可落地的参考:无需修改 PgBouncer 核心代码,只需调整部署架构和配置,即可将吞吐量提升 4 倍,同时更充分地利用云实例的 CPU 资源。

这一方案也体现了云原生环境下“分而治之”的扩展思路:将单进程的瓶颈转化为多进程的协作,并利用内核特性解决负载均衡,用 Peering 解决状态感知问题。对于其他单线程中间件(如 Redis、HAProxy 等)的横向扩展也有借鉴意义。此外,ClickHouse 将 Managed Postgres 与 ClickHouse 结合,形成统一的数据栈,降低了应用开发者的架构复杂度。用户只需部署一个 Postgres 实例,即可自动获得优化后的连接池配置,从而更专注于业务逻辑。

查看原文 →clickhouse.com