在Citus中使Postgres存储过程快9倍

2020-11-22 06:13:11

如果要了解有关Microsoft Azure上的Citus的更多信息,请阅读有关PostgreSQL的Azure数据库上的Hyperscale(Citus)的信息。

跳过导航存储过程在商业关系数据库中被广泛使用。您可以使用PL / SQL编写大多数应用程序逻辑,并将此逻辑推入数据库即可获得显着的性能提升。结果,希望从其他数据库迁移到PostgreSQL的客户通常会大量使用存储过程。

从大型数据库迁移时,使用Citus扩展程序分发数据库可能是一个有吸引力的选择,因为您将始终具有足够的硬件容量来支持工作负载。 PostgreSQL的Azure数据库中的Hyperscale(Citus)选项使在几分钟之内轻松获得托管的Citus群集变得容易。

过去,将存储过程迁移到Citus的客户通常会报告性能不佳,因为该过程中的每个语句都涉及Citus协调器节点和工作节点之间的额外网络往返。当我们使用HammerDB(TPROC-C)中基于TPC-C的工作负载评估Citus性能时,我们也观察到了这一点,该工作负载是使用存储过程实现的。

好消息是,这一切都在Citus 9.0中发生了变化,后者引入了一项称为“分布式功能”的功能。对于一类重要的工作负载,分布式功能可以消除存储过程中的大部分网络开销,并且在后续的Citus版本中,我们进一步改进了分布式Postgres功能。在HammerDB中,这些更改加起来使速度提高了一个数量级!

这篇博客文章将向您展示我们如何在Citus中实现如此显着的性能改进,以及为什么存储过程现在成为使用Citus在Postgres上扩展事务工作负载的强大技术。

在某些开发人员中,存储过程的声誉确实很差,这主要与使用存储过程实现业务逻辑的情况有关。在拥有外部基础结构(用于更新,日志记录,监视,调试,测试等)的数据库外部维护业务逻辑当然要容易得多。但是,使用存储过程并将业务逻辑放入数据库实际上是两件事。

存储过程可以帮助您为Postgres数据库创建定义明确的API。拥有定义良好的API意味着您可以将应用程序逻辑与数据库架构分离,并可以独立更新它们。否则,即使要做简单的事情(例如,在不停机的情况下更改列名)也非常困难。另外,您的应用程序逻辑和Postgres查询之间的映射变得非常简单,这有助于您简化代码。最重要的是,通过存储过程可以实现的效率提升太大,无法大规模忽略。

如果正确使用,Postgres中的存储过程将为您提供难得的机会来简化代码,简化操作并显着提高性能和可伸缩性。

也许,正如罗伯·科纳里(Rob Conery)雄辩地说的那样,是时候克​​服您对存储过程的厌恶了。

一个存储过程执行10个查询。在单个Postgres服务器上,每个查询的执行时间为1毫秒,因此整个过程需要10毫秒。

然后,我们决定将表分布在大型Citus集群上,这意味着我们可以将所有数据保留在内存中,并且查询可以在Citus worker节点上以0.1ms的速度执行,但是现在每个查询还涉及Citus协调器与工作节点,这意味着整个过程需要11毫秒,即使有很多额外的硬件也是如此。

为了解决这个问题,我们更加仔细地研究了客户的工作量。我们注意到,Citus上的存储过程通常会反复进行到同一Citus工作程序节点的网络往返。例如,SaaS /多租户应用程序中的存储过程通常仅在单个租户上运行。这意味着,从理论上讲,存储过程中的所有工作都可以委派给工作节点,该工作节点在一次网络往返中存储该租户的数据。

将存储过程调用委托给存储相关数据的节点并不是一个新主意。 Michael Stonebraker和PL / proxy共同开发的VoltDB完全围绕通过在分区边界内执行过程来扩展数据库的想法而设计,但是作为数据库系统,它们比Citus更具限制性。

Citus用户可以编写包含对分布式表的任意查询的PL / pgSQL存储过程(定义为函数或过程)。如前所述,存储过程通常在单个Citus工作节点上运行,但是没有什么阻止它们在多个工作节点上运行。

为了在不损失功能的情况下实现可伸缩性,我们引入了“分布式功能”的概念,该概念可以在任何工作节点上调用并可以执行任意查询。创建分布式函数时,Citus协调器节点会自动将其所有元数据复制到Citus工作器节点,以便工作器可以充当其接收的任何过程调用的协调器。

您还可以给分布式函数一个“ distribution arguments”,它对应于分布式表的“ distribution column”。在那种情况下,当在Citus协调器节点上调用函数或过程时,该调用将委派给工作节点,该工作节点存储由distribution参数给出的分发列值(例如,租户ID)。下图显示了调用传递函数的示例。

理想情况下,存储过程使用该参数作为所有查询中分发列上的过滤器,这意味着它仅需要访问工作节点本地的分片。如果没有,那没关系,因为每个Citus工作者节点都可以发起涉及其他工作者节点的分布式事务。

可以通过在现有函数和过程上调用create_distributed_function来创建分布式函数。例如,以下是在构建TPROC-C模式时我们用于分发HammerDB生成的表和过程的完整步骤集:

-仅在使用Citus开源时才需要激活元数据复制

设置citus。复制模型='流式';

-根据表的仓库ID列分配表(表自动并置)

SELECT create_distributed_table('customer','c_w_id');

SELECT create_distributed_table('district','d_w_id');

SELECT create_distributed_table('history','h_w_id');

选择create_distributed_table('warehouse','w_id');

SELECT create_distributed_table('stock','s_w_id');

SELECT create_distributed_table('new_order','no_w_id');

SELECT create_distributed_table('orders','o_w_id');

SELECT create_distributed_table('order_line','ol_w_id');

SELECT create_reference_table('item');

-使用第一个参数(仓库ID)作为分配参数来分配函数,并与仓库表共存

SELECT create_distributed_function('delivery(int,int)','$ 1',colocate_with:='仓库');

选择create_distributed_function('neword(int,int,int,int,int,int,int)','$ 1',colocate_with:='仓库');

SELECT create_distributed_function('payment(int,int,int,int,numeric,int,数字,varchar,varchar,数字)','$ 1',colocate_with:='仓库');

SELECT create_distributed_function('slev(int,int,int)','$ 1',colocate_with:='仓库');

SELECT create_distributed_function('ostat(int,int,int,int,varchar)','$ 1',colocate_with:='仓库');

-只需在所有节点上创建此函数(无分布参数)

SELECT create_distributed_function('dbms_random(int,int)');

完成这些步骤之后,每个存储过程调用都委派给Citus worker节点,该节点存储在第一个参数中指定的仓库ID。在大多数情况下,存储过程会将参数作为过滤器传递给这些分布式查询,这意味着几乎所有查询都可以在没有网络往返的情况下得到回答。

Neword和付款过程有时会访问多个仓库,这将导致Citus工人节点执行分布式交易。

当我们首次在Citus中对分布式函数进行原型设计时,它仍然存在一个主要的性能问题:工作节点正在与自身建立TCP连接以查询分片,从而导致大量开销并限制了并发。我们通过引入一种称为“本地执行”的补充技术解决了这一问题。

在探讨本地执行的工作原理之前,您可能应该了解Citus如何处理分布式表上的查询。

Citus查询计划程序通过PostgreSQL计划程序钩子拦截对分布式表的查询。 Citus分布式查询计划器根据查询中的分布列过滤器检测要访问哪些分片,然后为每个分片生成查询树。然后,将每个查询树“分解”为SQL文本,然后Citus查询计划者将查询计划移交给分布式查询执行器。执行程序使用PostgreSQL的标准连接库(libpq)将SQL查询发送到工作节点。每个Citus辅助节点执行其查询,然后将结果返回给Citus协调器节点。

这种方法对大多数查询都有效,因为只有协调器才能连接到工作节点。但是,使用分布式功能方法,Citus协调器将连接到Citus工作程序节点,然后Citus工作程序节点将连接到它自己。在Postgres中,连接是稀缺资源,因此这种方法限制了可实现的并发性。

幸运的是,Citus工作节点实际上并不需要单独的TCP连接来查询分片,因为分片与存储过程位于同一数据库中。因此,我们引入了本地执行,以通过发出函数调用的同一连接在本地函数内执行Postgres查询。下图概述了更改前后的Citus连接逻辑。

通过引入本地执行,分布式函数产生的开销更少,并且由于它使所有连接插槽都可用于存储过程调用,因此我们可以实现更高的并发性。在Citus 9.0中进行此更改之后,吞吐量仅受到工作人员数量的限制。

在运行HammerDB基准测试时,我们确实发现与常规PostgreSQL服务器相比,Citus辅助节点效率相对较低。因此,我们实现了另一个优化:计划缓存。

PostgreSQL具有准备好的语句的概念,它使您可以为多次执行缓存查询计划,而无需多次分析和计划同一查询的开销。 Postgres计划器尝试使用通用计划,该计划在执行5次之后适用于任何参数值,如下图所示:

用PL / pgSQL编写存储过程的好处之一是Postgres自动将每个SQL查询转换为准备好的语句,但是这种逻辑并没有立即对Citus有所帮助。原因是Citus工作程序节点包含多个分片,而Citus不知道要预先查询哪个。

解决方案很简单:我们在分布式查询的计划中缓存每个本地分片的Postgres查询计划,而分布式查询计划由准备好的语句逻辑缓存。由于Citus中的本地分片数量通常很少,因此只会产生少量的内存开销。

我们在Citus 9.0中发布了分布式功能和本地查询执行更改,在Citus 9.2中发布了计划缓存,并在Citus 9.4版本中提高了性能。

在PostgreSQL的Azure数据库上的10节点超大规模(Citus)群集上使用HammerDB基准测试工具运行TPROC-C工作负载,每个节点16个vcore:

在Citus 9.0中,使用分布式功能时将NOPM从〜50k改进为320K。 NOPM指每分钟的新订单交易(约占交易总数的43%)。

下表HammerDB TPROC-C的性能直观地展示了我们如何改善这些不同的Citus开源版本的Citus存储过程性能(以PostgreSQL的Azure数据库中的Hyperscale(Citus)衡量)。

所有这些更改的巧妙之处在于,我们从一个在Citus中最好避免使用存储过程的地方开始,然后到达一个存储过程是在Citus上扩展Postgres OLTP工作负载的强大方式的地方。

PostgreSQL中的存储过程非常有用:它们可以为您提供自动计划缓存,帮助您避免Citus中的网络往返,并且您可以在单个分布式事务中更改存储过程以及模式(最后一点仍然令人难以置信)因此,如果您要将过程从商业数据库迁移到Postgres,或者发现Postgres存储过程是一个有用的原语,并且您需要存储过程超快速和可扩展,我们建议您使用Citus中新的分布式功能。

特别要感谢Splendid Data,他在去年关于Oracle-> Citus迁移的集思广益会议上帮助我们提出了分布式功能的想法。

如果您有兴趣阅读我们团队的更多帖子,请注册我们的每月新闻,并将最新内容直接发送到您的收件箱。