如何在ClickHouse Managed Postgres中扩展PgBouncer
PgBouncer单线程运行,无论服务器有多少CPU核心,单个进程仅使用一个核心。在16个虚拟CPU的服务器上,一个核心承担所有连接池管理工作,其余15个核心闲置。并且,在Postgres还有处理能力时,连接池就开始限制吞吐量。
在ClickHouse Managed Postgres中,运行一组与可用核心数成正比的PgBouncer进程。这组进程中的每个进程都使用启用了 `so_reuseport` 的相同端口进行绑定。内核会将传入的连接负载均衡到各个进程上,客户端仍连接到单个端点,不会察觉到背后有多个PgBouncer进程。这正是PgBouncer官方文档中提到的利用多个核心的机制:每个进程是单线程的,而 `so_reuseport` 可以让每个核心都发挥作用。
问题:查询取消
Postgres的取消请求会通过一个全新的连接发送,该连接携带一个取消密钥,与运行查询的连接分开。使用 `so_reuseport` 时,内核可能会将这个新连接分配给与持有会话的进程不同的进程。这样,取消请求就会发送到一个从未听说过该查询的进程上,导致取消操作失败。
通过进程间的“对等机制(peering)”可以解决这个问题。这些进程相互了解,若取消请求发送到了错误的进程,它会被转发到实际持有会话的进程。这样,即使任何给定的请求可能到达任意进程,整个进程组都能正常处理取消操作。
连接池以事务模式运行,一旦事务提交,服务器连接就会返回到连接池。并且连接预算会在整个进程组中进行分配:`max_client_conn` 和 `max_db_connections` 会除以进程数量,确保整个进程组不会使Postgres过载。
在真实硬件上的测试结果
在相同的AWS EC2实例上运行了两种配置:一个16虚拟CPU的 `c7i.4xlarge` 实例用于运行连接池,另一个单独的实例运行Postgres,还有一个实例使用 `pgbench` 在仅查询、事务池模式下施加负载。一个连接池实例运行单个PgBouncer进程,另一个运行16个进程组成的进程组。实例类型、Postgres、工作负载均相同,唯一的变量就是单个进程与16个进程的区别。
将客户端连接数从8增加到256,并测量了吞吐量以及每个连接池实际使用的16核心服务器的资源情况。
单个进程在大约87000笔交易/秒时达到峰值,之后随着负载增加性能反而下降。当客户端数量达到256时,吞吐量降至77000笔交易/秒,因为所有操作都在争夺一个核心的资源。而进程组的吞吐量持续上升,最终达到约336000笔交易/秒,大约是单个进程的4倍,因为它可以利用更多的核心。
单个进程的工作负载从未超过一个核心:在负载下,`pidstat` 显示PgBouncer进程的CPU使用率固定在约97%,即一个完整的核心,而16虚拟CPU的服务器整体利用率仍低于10%。进程组则可以充分利用服务器资源,大约有8个核心处于忙碌状态,并且在Postgres和负载生成器成为瓶颈之前,还有提升空间。
保持256个客户端对每个实例的稳定连接:单进程实例在整个运行过程中的CPU使用率接近9%,而进程组的CPU使用率约为52%。实例类型、Postgres、工作负载相同,但一种配置使服务器闲置,另一种则充分利用了服务器资源。
EC2的CloudWatch指标从外部也反映了相同的情况:在负载期间,单进程实例的平均CPU利用率约为16%,而进程组约为60%。CloudWatch的读数比实例内部的读数略高,但差距依然存在:在为16个虚拟CPU付费的服务器上,单个PgBouncer几乎让所有资源都闲置了。
连接上限的情况也是如此。单个进程会自行实施 `max_client_conn` 限制,一旦超过这个限制,新的客户端就会被拒绝连接:
FATAL: no more connections allowed (max_client_conn)
将连接预算分配到进程组中,可以在保证每个进程和Postgres处于安全限制范围内的同时,提高总体连接上限。
| 客户端数量 | 单进程TPS | 单进程实例CPU | 进程组TPS | 进程组实例CPU |
|---|---|---|---|---|
| 8 | 8910 | 0.8% | 6450 | 2.9% |
| 32 | 54203 | 5.2% | 64244 | 12.3% |
| 64 | 86570 | 8.3% | 219439 | 31.9% |
| 128 | 83463 | 8.1% | 320547 | 45.9% |
| 256 | 76893 | 7.7% | 336469 | 48.9% |
在连接数较少时,单个进程的表现实际上还不错,甚至稍微快一些,因为此时没有并行操作,而进程组的连接分布较分散。但在真正的并发场景下,差距就会显现出来,因为单个核心会成为性能瓶颈。
总结
在连接池(而非Postgres)成为吞吐量瓶颈之前,单个PgBouncer是一个不错的默认选择。根据核心数量调整进程组的大小,使用 `so_reuseport` 共享一个端口,并通过对等机制将进程连接起来,可以让连接池不再成为瓶颈,而是高效的基础组件。
每个ClickHouse Managed Postgres服务器默认都采用这种配置。可以创建一个Postgres实例来体验一下。
试用ClickHouse管理的Postgres
ClickHouse + Postgres已成为可扩展应用程序的统一数据栈。随着ClickHouse Cloud中托管Postgres的推出,可以从一开始就选择这个强大的数据栈。
立即注册
立即开始使用
想了解ClickHouse如何处理数据吗?只需几分钟即可开始使用ClickHouse Cloud,并获得300美元的免费信用额度。
立即注册