PgBouncer 是 PostgreSQL 生态中最流行的连接池工具,但有一个众所周知的瓶颈:它是单线程的。在一个 16 核的机器上,PgBouncer 只能用一个核心做连接池管理,其余 15 个核心完全闲置。ClickHouse 的托管 Postgres 团队最近分享了他们如何通过巧妙的多进程架构将 PgBouncer 的单机吞吐量提升至 4 倍。
so_reuseport + Peering:核心思路
方案的核心原理简洁而优雅:在每个核心上运行一个独立的 PgBouncer 进程,所有进程绑定同一端口并启用 so_reuseport。Linux 内核会自动将进入的连接均匀分配到各个进程——客户端只看到一个端点,完全感知不到背后有多个 PgBouncer 实例。
这正是 PgBouncer 官方文档推荐的横向扩展方式。但实施过程中有一个关键陷阱。
查询取消的致命问题
PostgreSQL 的查询取消机制带来了麻烦:取消请求会发起一个全新的 TCP 连接,携带一个 cancel key。而 so_reuseport 的内核负载均衡可能把这个新连接交给任何一个进程——如果取消请求落在非目标进程上,查询永远不会被取消,用户只能干等超时。
ClickHouse 团队的解决方案是 Peer 机制:所有 PgBouncer 进程通过 Unix socket 互相感知。当一个取消请求落在错误进程上时,该进程会将请求转发到真正持有该会话的进程。对用户而言,取消操作完全透明——无论请求被内核路由到哪个进程,都能正确生效。
配置与实战效果
关键的配置片段(HN 用户热心补全):
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
so_reuseport = 1
peer_id = 1
unix_socket_dir = /tmp/pgbouncer1
[peers]
1 = host=/tmp/pgbouncer1
2 = host=/tmp/pgbouncer2
3 = host=/tmp/pgbouncer3
4 = host=/tmp/pgbouncer4
连接池运行在 transaction 模式下,每个事务提交后服务端连接立即归还池中。连接预算(max_client_conn / max_db_connections)被均分到各进程。整体效果:单机吞吐量提升至原来的 4 倍。
社区反响与扩展思路
HN 评论区的技术讨论非常活跃。有用户分享了在 Kubernetes 中运行多 PgBouncer 的经验——利用 K8s 的多容器 Pod 实现类似效果,还能跨机器扩展以应对 Azure 的滚动宕机。另一位开发者分享了一个有趣的类比:他在 BitTorrent 客户端中实现了类似的 so_reuseport + rendezvous 机制,证明这个模式在连接密集型场景中具有通用性。
这篇来自 ClickHouse 的分享不仅解决了一个实际工程问题,更展示了一个可迁移的架构模式:单线程工具 → so_reuseport 多实例 → peer 协调层。如果你的技术栈中有类似的单线程瓶颈,这个模式值得一试。
原文链接:How we scale PgBouncer in ClickHouse Managed Postgres
HN 讨论:Hacker News (166 points, 28 comments)