让Postgres快30%的方法


任何一个大规模运行Postgres的人都知道,性能是有代价的。典型的玩法是在数据库前放置一个像PgBouncer这样的池子,并打开事务模式。这使得多个客户端可以重复使用同一个服务器连接,这使得成千上万的客户端可以连接到你的数据库,而不会引起分叉炸弹。

不幸的是,这是有代价的。

由于多个客户端使用同一台服务器,他们无法利用prepared语句的优势。

prepared语句是Postgres缓存查询计划的一种方式,可以用不同的参数多次执行。
如果你以前没有尝试过,你可以针对你的本地DB运行pgbench,你会发现--protocol prepared比simple和extended至少要好30%。

在我的记忆中,放弃这个功能一直是生产部署中的必然选择,但现在不是了。

PgCat Prepared 语句
#474开始,PgCat支持会话和事务模式下的prepared 语句。

我们最初的基准测试显示,与扩展协议(--protocol extended)相比,性能提高了30%,与简单协议(--simple)相比,性能提高了15%。

大多数(所有的?)Web框架至少使用扩展协议,所以我们看到,对于每个编写Web应用并在生产中使用Postgres的人来说,只要切换到命名的prepared的语句,性能就会全面提高30%。

在Rails应用程序中,这就像设置prepare_statements: true一样简单。

这不仅是性能上的好处,而且对那些必须使用prepared 的语句的客户端库,如流行的Rust crate SQLx,也是一种可用性的改进。到目前为止,典型的建议是不要使用线程池pooler。

基准测试
基准测试使用pgbench进行,1、10、100和1000个客户端向PgCat发送数百万次查询,PgCat本身与数据库一起在不同的EC2机器上运行。

这是生产中经常使用的一个简单设置。

另一种配置是池子使用自己的机器,这当然会增加延迟,但会提高可用性。客户端在另一台EC2机器上,以模拟部署在Kubernetes、ECS、EC2和其他系统中的典型网络应用所经历的延迟。

基准在事务模式下运行。会话模式在客户端较少的情况下速度较快,但在生产中,如果客户端超过几百个,则无法扩展。只使用了SELECT语句(-S选项),因为典型的pgbench基准测试使用了类似数量的写和读,这是非典型的生产工作负载。大多数应用程序在90%的时间里都在读,10%的时间里在写。读取是prepared 的语句真正发光的地方。

在prepared方式下,如图所示性能提升30%:


点击标题见原图片

实施
PgCat在客户的prepared的语句和服务器之间实现了一个内部缓存和映射。

  • 如果服务器有prepared的语句,PgCat只是转发Bind (F)、Execute (F)和Describe (F)消息。
  • 如果服务器没有prepared的语句,PgCat会从客户端的缓存中获取它,并使用Parse (F)消息来准备它。

你可以参考Postgres的文档 Postgres docs,了解更多关于扩展协议工作原理的详细解释。

PgCat实现的一个重要特点是,所有prepared的语句都被重新命名并分配给全局唯一的名字。

这意味着那些没有随机化prepared的语句名称并期望在与 "Postgres服务器 "断开连接后就消失的客户端,可以像预期的那样工作(我给 "Postgres服务器 "加上引号是因为他们实际上是在与一个假装是Postgres数据库的代理对话)。
当使用这种客户端和PgBouncer时,典型的错误是prepared的语句 "sqlx_s_2 "已经存在,当你第一次看到它时,这是很令人困惑的。

衡量标准
我们在管理数据库中增加了两个新的指标:prepare_cache_hit 和 prepare_cache_miss。

  • Prepare cache hits表示客户端请求的prepare语句已经存在于服务器上。这很好,因为PgCat可以直接重写信息并立即将它们发送到服务器上。
  • prepare_cache_miss表示PgCat不得不向服务器发出prepared的语句调用,这需要额外的时间并降低了吞吐量。在理想的情况下,高速缓存的点击率要比高速缓存的失误率高出一个数量级。如果它们相同或更差,说明客户没有正确使用prepared的语句。

我们的基准是99.99%的缓存命中率,这确实很好,但在生产中这个数字可能会更低。

你可以通过管理数据库来监控你的缓存命中率/失误率,方法是通过SHOW SERVERS查询.