文章摘要
ClickHouse Managed Postgres通过运行多个PgBouncer进程并启用so_reuseport,让内核在进程间负载均衡连接,从而突破单线程瓶颈,充分利用多核CPU提升连接池性能。
文章总结
好的,这是根据您的要求,对原文进行的中文重述:
标题:在 ClickHouse 托管 Postgres 中如何扩展 PgBouncer
PgBouncer 是单线程的,无论机器有多少个 CPU 核心,单个进程只能使用一个核心。例如,在一台 16 vCPU 的机器上,一个核心负责所有连接池工作,其余十五个核心闲置,导致连接池的吞吐量远在 Postgres 数据库达到极限之前就遇到了瓶颈。
在 ClickHouse 托管 Postgres 服务中,我们运行着一组 PgBouncer 进程,其数量与可用核心数成正比。该组中的每个进程都通过启用 so_reuseport 选项来绑定同一个端口。这样,操作系统内核会将新来的连接请求负载均衡地分配给组内的各个进程。因此,客户端只需连接到一个单一端点,完全不知道背后有多个 PgBouncer 在协同工作。这正是 PgBouncer 官方文档所推荐的利用多核的方法:每个进程是单线程的,而 so_reuseport 则是让所有核心都工作起来的关键。
一个 Postgres 取消请求会通过一个全新的连接发送,并携带一个取消密钥,这与执行查询的连接是分开的。使用 so_reuseport 时,内核可能会将这个新连接分配给一个与持有该会话的进程不同的进程。如果取消请求落到了一个从未听说过该查询的进程上,那么取消操作将不会生效。
“对等互联”功能解决了这个问题。组内的进程彼此感知,因此,如果取消请求被错误地分发,它会被转发给实际拥有该会话的进程。这样一来,即使任何请求可能到达组内的任意进程,取消操作在整个进程组中都能正常工作。
连接池以事务模式运行,因此一旦事务提交,服务器连接就会立即返回给连接池。连接预算在进程组内进行分配:max_client_conn 和 max_db_connections 这两个参数的值会除以进程数量,从而确保整个进程组不会向 Postgres 数据库提交超出其处理能力的连接请求。
我们在相同的 AWS EC2 实例上对两种配置进行了测试:一个 16 vCPU 的 c7i.4xlarge 实例作为连接池服务器,另一台独立的服务器运行 Postgres,还有第三台服务器使用 pgbench 在只读、事务池模式下生成负载。一个连接池服务器运行单个 PgBouncer 进程;另一个则运行一组共 16 个进程。除了进程数量这个变量外,实例类型、Postgres 配置和工作负载都完全相同。
我们将客户端连接数从 8 个逐步增加到 256 个,并测量了吞吐量以及每个连接池服务器对这台 16 核机器的实际利用率。
单个进程的吞吐量在大约 87,000 事务/秒时达到峰值,随后在更高负载下性能反而下降,在 256 个客户端时滑落至 77,000 事务/秒,这是因为所有请求都在争抢一个核心。而进程组的吞吐量则持续攀升至大约 336,000 事务/秒,性能提升了约 4 倍,因为它可以利用更多的核心。
单个进程从未使用超过一个核心的计算能力:在负载下,pidstat 显示 PgBouncer 进程的 CPU 占用率固定在约 97%,占满了一个核心,而整个 16 vCPU 机器的利用率却低于 10%。进程组则将负载分散到整个机器上,达到了约 8 个核心忙碌的状态,并且当 Postgres 和负载生成器成为新的瓶颈时,它仍有富余的处理能力。
让 256 个客户端分别稳定地连接两个服务器:单进程服务器的 CPU 在整个运行期间接近 9%,而进程组服务器的 CPU 则稳定在 52% 左右。相同的实例类型、相同的 Postgres、相同的工作负载。一种配置让机器闲置,另一种则让它高效运转。
EC2 自身的 CloudWatch 指标也从虚拟机外部证实了这一点:在负载期间,单进程实例的平均 CPU 利用率约为 16%,而进程组实例约为 60%。虽然 CloudWatch 的读数略高于虚拟机内部的数据,但两者之间的巨大差距是一致的:在一台你支付了 16 个 vCPU 费用的机器上,单个 PgBouncer 几乎浪费了所有的计算能力。
连接上限的情况也是如此。单个进程自行执行 max_client_conn 的限制,一旦超过这个限制,新的客户端就会被拒绝连接。将连接预算分配到整个进程组,可以让你在提高总连接上限的同时,确保每个进程和 Postgres 数据库都保持在安全范围内。
| 客户端数 | 单进程 TPS | 单进程服务器 CPU | 进程组 TPS | 进程组服务器 CPU | | :--- | :--- | :--- | :--- | :--- | | 8 | 8,910 | 0.8% | 6,450 | 2.9% | | 32 | 54,203 | 5.2% | 64,244 | 12.3% | | 64 | 86,570 | 8.3% | 219,439 | 31.9% | | 128 | 83,463 | 8.1% | 320,547 | 45.9% | | 256 | 76,893 | 7.7% | 336,469 | 48.9% |
在只有少量连接时,单进程的表现实际上还不错,甚至略快一些,因为此时没有并行化的必要,而进程组的连接被分散得比较稀疏。性能差距恰恰在真正重要的场景下显现:在高并发下,单个核心成为了瓶颈。
单个 PgBouncer 在默认情况下是一个不错的选择,直到连接池本身,而非 Postgres,成为了限制吞吐量的因素。根据核心数量来配置进程组、使用 so_reuseport 共享一个端口,并通过“对等互联”将进程连接起来,这能将连接池从瓶颈变回一个顺畅的管道。
每个 ClickHouse 托管 Postgres 服务器都默认配备了这种设置。您可以配置一个 Postgres 实例来体验它的效果。
评论总结
根据评论内容,主要观点和论据如下:
1. 配置与实现细节(认可度:中性) - 评论1建议文章应展示具体配置,并提供了PGBouncer的示例配置,包括listenaddr、soreuseport和peers设置。 - 关键引用:"Article should show the config: [pgbouncer] listen_addr = 0.0.0.0 ... [peers] 1 = host=/tmp/pgbouncer1"
2. 实际应用场景(认可度:正面) - 评论2提到在Kubernetes上运行PGBouncer,可轻松实现单机多进程或多机部署,有助于应对Azure的滚动维护。 - 关键引用:"We run pgbouncer via kubernetes so it was straightforward to make multiple pgbouncer processes on one machine."
3. 替代方案与工具(认可度:正面) - 评论5推荐Odyssey作为可扩展的PGBouncer替代品:"Just use https://github.com/yandex/odyssey :) It's a scalable PgBouncer." - 评论6推荐pgdog,称其效果良好:"I've been using pgdog and it has worked really well for my needs!"
4. 技术探索与扩展(认可度:中性偏正面) - 评论3分享了在rqbit BitTorrent客户端中类似so_reuseport的实践,通过peering系统实现实例间通信,认为这种协调机制有趣但偏实验性质。 - 关键引用:"This was more for fun than real use... It'd be really neat to have some kind of general peering protocol that different apps could use."
5. 架构对比(认可度:中性) - 评论4提出疑问:使用HAProxy加多个PGBouncer实例是否有劣势?"Was there a disadvantage to using HAProxy + multiple PGBouncer instances?"
总结: 评论主要围绕PGBouncer的soreuseport配置、实际部署经验、替代工具(如Odyssey、pgdog)以及技术探索展开。多数评论对soreuseport的协调机制持正面态度,但部分用户认为有更成熟的替代方案。