我们将 PgBouncer 的吞吐量提升了 4 倍。
We scaled PgBouncer to 4x throughput

原始链接: https://clickhouse.com/blog/pgbouncer-clickhouse-managed-postgres

PgBouncer 是单线程的,这意味着单个进程只能利用一个 CPU 核心,从而在多核硬件上造成吞吐量瓶颈。为了克服这一问题,ClickHouse Managed Postgres 部署了一组 PgBouncer 进程,并利用 `so_reuseport` 允许内核将传入的连接负载均衡到所有可用核心上。 这种架构给 Postgres 取消请求带来了挑战,因为请求可能会到达一个并不持有目标会话的进程。为了解决这个问题,该集群使用了一种“对等”(peering)机制,将取消请求转发给正确的进程。此外,连接限制会在集群中进行智能分配,以防止数据库过载。 性能基准测试证实了该方法的有效性:单进程配置的上限是一个核心的容量,而集群架构通过利用多个核心显著扩展了吞吐量。在高并发环境下,与相同硬件上的单进程设置相比,该集群实现的每秒事务处理量约为前者的 4 倍。通过将连接池从单线程瓶颈转变为分布式多核系统,ClickHouse 确保了连接池能够有效处理高并发工作负载,同时不会浪费宝贵的计算资源。

Hacker News最新 | 过往 | 评论 | 提问 | 展示 | 招聘 | 提交登录我们将 PgBouncer 的吞吐量扩展到了 4 倍 (clickhouse.com)25 分,saisrirampur 发布于 30 分钟前 | 隐藏 | 过往 | 收藏 | 1 条评论帮助 nosefrog 0 分钟前 | 下一条 [–] 有意思。我们通过 Kubernetes 运行 PgBouncer,所以在单台机器上创建多个 PgBouncer 进程非常简单。同样,将它们部署在多台机器上也十分直接,这对我们很有帮助,因为我们在 Azure 上运行,而他们喜欢通过虚拟机维护在我们的整个集群中造成滚动中断……回复 准则 | 常见问题 | 列表 | API | 安全 | 法律 | 申请 YC | 联系 搜索:
相关文章

原文

PgBouncer is single-threaded. A single process uses one CPU core, no matter how many the machine has. On a 16-vCPU box that means one core does all the connection pooling while the other fifteen sit idle, and the pooler starts capping throughput long before Postgres runs out of room.

In ClickHouse Managed Postgres we run a fleet of PgBouncer processes, sized proportional to the available cores.

Every process in the fleet binds the same port with so_reuseport enabled. The kernel load-balances incoming connections across the processes, so clients still connect to a single endpoint and never know there is more than one PgBouncer behind it. This is the mechanism PgBouncer's own docs point to for using more than one core: it is single-threaded per process, and so_reuseport is how you put every core to work.

pgbouncer_jul2026_image5.png

A Postgres cancel request arrives on a brand-new connection carrying a cancel key, separate from the connection running the query. With so_reuseport, the kernel is free to hand that new connection to a different process than the one holding the session. The cancel lands on a process that has never heard of the query, and nothing happens.

Peering fixes this. The processes are aware of one another, so a cancel that lands on the wrong process is forwarded to the one that actually owns the session. Cancellation works across the whole fleet, even though any given request can arrive anywhere.

Pooling runs in transaction mode, so a server connection is returned to the pool the moment a transaction commits. And the connection budget is split across the fleet: max_client_conn and max_db_connections are divided by the number of processes, so the fleet as a whole never oversubscribes Postgres.

We ran both configurations on identical AWS EC2 instances: a 16-vCPU c7i.4xlarge for the pooler, a separate box for Postgres, and a third driving load with pgbench in select-only, transaction-pooled mode. One pooler box ran a single PgBouncer process; the other ran a fleet of 16. Same instance type, same Postgres, same workload. The only variable is one process versus sixteen.

We ramped client connections from 8 to 256 and measured throughput and how much of the 16-core box each pooler actually used.

The single process peaks around 87k transactions/sec and then gets worse under more load, sliding to 77k at 256 clients as everything contends for one core. The fleet keeps climbing to roughly 336k transactions/sec, about 4x, because it has more cores to climb into.

The single process never gets past about one core of work: under load, pidstat shows the PgBouncer process pinned at ~97% CPU, a full core, while the 16-vCPU box as a whole stays under 10% utilized. The fleet spreads across the machine, reaching roughly 8 cores busy, and it still had headroom when Postgres and the load generator became the limit.

Hold 256 clients steady against each box: the single-process box runs near 9% CPU for the entire run while the fleet holds around 52%. Same instance type, same Postgres, same workload. One configuration leaves the machine idle, the other puts it to work.

EC2's own CloudWatch metric says the same thing from outside the guest: during the load the single-process instance averages about 16% CPUUtilization, the fleet about 60%. CloudWatch reads a little higher than the in-guest number, but the same gap holds: on a box you're paying 16 vCPUs for, a single PgBouncer leaves almost all of it on the floor.

The connection ceiling behaves the same way. A single process enforces max_client_conn on its own, and once you cross it, new clients are turned away:

FATAL:  no more connections allowed (max_client_conn)

Splitting the budget across the fleet is what lets you raise the aggregate ceiling while keeping each process, and Postgres, within safe limits.

ClientsSingle TPSSingle box CPUFleet TPSFleet box CPU
88,9100.8%6,4502.9%
3254,2035.2%64,24412.3%
6486,5708.3%219,43931.9%
12883,4638.1%320,54745.9%
25676,8937.7%336,46948.9%

At a handful of connections the single process is actually fine, even a hair faster, since there's nothing to parallelize and the fleet's connections are spread thin. The gap opens exactly where it matters: under real concurrency, where one core becomes the wall.

A single PgBouncer is a fine default until the pooler, not Postgres, is what caps your throughput. Sizing a fleet to the cores, sharing one port with so_reuseport, and wiring the processes together with peering turns the pooler back into plumbing instead of a bottleneck.

Every ClickHouse Managed Postgres server ships with this setup by default. Provision a Postgres and see it in action.

联系我们 contact @ memedata.com