当前位置: 首页 > 科技观察

分布式PostgreSQL集群(Citus),分布式表中的分布式列选择最佳实践

时间:2023-03-14 22:30:20 科技观察

确定应用程序类型在Citus集群上运行高效查询需要数据在机器之间正确分布。这因应用程序类型及其查询模式而异。基本上有两种类型的应用程序可以很好地与Citus配合使用。数据建模的第一步是确定哪些应用程序类型更接近您的应用程序。概述多租户应用程序实时应用程序有时模式中有数十个或数百个表。桌子的数量很少。一次与一个租户(公司/商店)相关的查询。具有服务聚合的相对简单的分析查询。Web客户端的OLTP工作负载摄取大量几乎不可变的数据OLAP工作负载为每个租户提供分析查询服务通常围绕大型事件表实例并表征多租户应用程序这些通常是SaaS应用程序服务其他公司、账户或组织。大多数SaaS应用程序本质上都是关系型的。它们具有跨节点分布数据的自然维度:仅按tenant_id进行分片。Citus使您能够将数据库扩展到数百万租户,而无需重建您的应用程序。您可以保留所需的关系语义,例如连接、外键约束、事务、ACID和一致性。示例:托管其他企业店面的网站,例如数字营销解决方案或销售自动化工具。特点:查询与单个租户相关,而不是跨租户连接信息。这包括为Web客户端提供服务的OLTP工作负载,以及为每个租户提供分析查询服务的OLAP工作负载。在您的数据库模式中拥有数十个或数百个表也是多租户数据模型的一个指标。使用Citus扩展多租户应用程序也需要对应用程序代码进行最少的更改。我们支持流行的框架,例如RubyonRails和Django。需要大规模并行性的实时分析应用程序,协调数百个内核以快速获取数值、统计或计数查询结果的应用程序。通过跨多个节点对SQL查询进行分片和并行化,Citus可以在一秒内对数十亿条记录执行实时查询。示例:需要亚秒级响应时间的面向客户的分析仪表板。特征:几个表,通常以设备、站点或用户事件的大表为中心,并且需要大量摄取大部分不可变的数据。涉及多个聚合和GROUPBY的相对简单(但计算量大)的分析查询。如果您的情况类似于上述任何一种情况,下一步就是决定如何在您的Citus集群中分片数据。如概念部分所述,Citus根据表的分布列的哈希值将表行分配给分片。DBA对分布列的选择需要匹配典型查询的访问模式以确保性能。选择分布列Citus使用分布式表中的分布列将表行分布到分片。为每个表选择分布列是最重要的建模决策之一,因为它决定了数据如何跨节点分布。如果分布列选择正确,相关数据会在相同的物理节点上组合在一起,从而加快查询速度并增加对所有SQL功能的支持。如果列选择不正确,系统将运行不必要的缓慢并且无法支持所有跨节点的SQL功能。本节为两种最常见的Citus场景提供分布列提示。最后,它深入探讨了协同定位,即节点上数据的理想分组。多租户应用程序多租户架构使用一种分层数据库建模的形式来跨分布式集群中的节点分布查询。数据层次结构的顶部称为租户ID,需要存储在每个表的一列中。Citus检查查询以查看它们引用的租户ID,并将查询路由到单个工作节点进行处理,特别是持有与租户ID关联的数据分片的节点。运行将所有相关数据放在同一节点上的查询称为表共置。下图说明了多租户数据模型中的协同定位。它包含两个表,Accounts和Campaigns,每个表都由account_id分配。阴影框代表分片,每个分片的颜色表示哪个工作节点包含它。绿色分片一起存储在一个工作节点上,蓝色分片存储在另一个节点上。请注意,当将两个表限制为相同的account_id时,Accounts和Campaigns之间的连接查询如何将所有必要的数据放在一个节点上。要在您自己的架构中应用此设计,第一步是确定应用程序中租户的构成。常见示例包括公司、帐户、组织或客户。列名类似于company_id或customer_id。检查您的每个查询并问问自己:如果它有一个额外的WHERE子句将所有涉及的表限制为具有相同租户ID的行,它会工作吗?多租户模型中的查询通常是租户范围内的,例如,将在某个商店内进行销售或库存查询。按公共tenant_id列对分布式表进行分区的最佳实践。例如,在租户为公司的SaaS应用程序中,tenant_id可能是company_id。将小型跨租户表转换为引用表。当多个租户共享一个小信息表时,将其作为参考表分发。限制通过tenant_id过滤所有应用程序查询。每个查询应一次请求一个租户的信息。阅读多租户应用程序指南,了解构建此类应用程序的详细示例。实时应用虽然多租户架构引入了层次结构并使用数据协同定位来路由每个租户的查询,但实时架构依赖于其数据的特定分布属性来实现高度并行处理。我们使用“实体id”作为实时模型中分布列的术语,而不是多租户模型中的租户id。典型的实体是用户、主机或设备。实时查询通常需要按日期或类别分组的数字聚合。Citus将这些查询发送到每个分片以获得部分结果,并在协调器节点上组装最终答案。当尽可能多的节点做出贡献并且没有单个节点必须执行不成比例的工作量时,查询运行得最快。最佳实践是选择基数高的列作为分布列。相比之下,订单表上的状态字段有新的、已付款的和已发货的值,对于分布列来说是一个糟糕的选择,因为它只假定这几个值。不同值的数量限制了可以保存数据的分片数量和可以处理数据的节点数量。在基数高的列中,最好额外选择那些在group-by子句中经常使用或作为连接键的列。选择均匀分布的列。如果您将表分布在偏向某些公共值的列上,表中的数据将倾向于累积在某些分片中。持有这些分片的节点最终会比其他节点做更多的工作。将事实表和维度表分布在它们的公共列上。您的事实表只能有一个分布键。连接到另一个键的表不会与事实表位于同一位置。根据连接的频率和连接行的大小,选择一个维度进行共置。将一些维度表更改为引用表。如果维度表不能与事实表共存,可以通过将维度表的副本作为参考表分发到所有节点来提高查询性能。阅读实时仪表板指南,了解构建此类应用程序的详细示例。时间序列数据在时间序列工作负载中,应用程序查询最近的信息,同时归档较早的信息。在Citus中建模时间序列信息时最常见的错误是使用时间戳本身作为分布列。基于时间的哈希分布将看似随机的时间分布在不同的分片上,而不是将时间范围保持在分片内。然而,涉及时间的查询通常指的是时间范围(例如最近的数据),因此这种散列分布会产生网络开销。最佳做法是不要选择时间戳作为分布列。选择不同的分布列。在多租户应用程序中,使用租户ID,或者在实时应用程序中,使用实体ID。请改用PostgreSQL表分区。使用表分区将一个大的按时间排序的数据表分解成多个继承表,每个表包含不同的时间范围。在Citus中分发Postgres分区表会为继承的表创建分片。阅读时间序列数据指南,了解构建此类应用程序的详细示例。表托管关系数据库由于其巨大的灵活性和可靠性而成为许多应用程序的首选数据存储。从历史上看,对关系数据库的一个批评是它们只能在一台机器上运行,当数据存储需要超过服务器改进时,就会产生固有的局限性。快速扩展数据库的解决方案是分布它们,但这会产生其自身的性能问题:连接等关系操作需要跨越网络边界。协同定位是对数据进行战略性分区的做法,将相关信息保存在同一台机器上以进行高效的关系操作,同时利用整个数据集的水平可扩展性。数据共存的原则是数据库中的所有表都有一个共同的分布列,并且以相同的方式跨机器进行分片,使得具有相同分布列值的行始终在同一台机器上,甚至通过这种方式跨不同的表。只要分布列提供有意义的数据分组,就可以在组内执行关系操作。Citus中用于哈希分布式表的数据共存Citus对PostgreSQL的扩展在能够形成数据库的分布式数据库方面是独一无二的。Citus集群中的每个节点都是一个功能齐全的PostgreSQL数据库,Citus在其之上添加了单一同构数据库的体验。虽然它不以分布式方式提供PostgreSQL的全部功能,但在许多情况下,它可以托管在一台机器上以充分利用PostgreSQL提供的功能,包括完整的SQL支持、事务和外键。在Citus中,如果分布列中值的散列落在分片的散列范围内,则行存储在分片中。为了确保并置,即使在重新平衡操作之后,具有相同哈希范围的分片也始终放置在同一节点上,这样相等的分布列值始终在跨表的同一节点上。我们发现在实践中运行良好的分布列是多租户应用程序中的租户ID。例如,SaaS应用程序通常有许多租户,但它们进行的每个查询都特定于该特定租户。虽然一种选择是为每个租户提供一个数据库或模式,但它通常成本高昂且不切实际,因为可能存在许多跨用户操作(数据加载、迁移、聚合、分析、模式更改、备份等)。随着租户数量的增加,这变得更难管理。协同定位的实际示例考虑下表,它们可能是多租户Web分析SaaS的一部分:创建表页面(tenant_idint,page_idint,路径文本,主键(tenant_id,page_id));现在我们想要回答可能由面向客户的仪表板发出的查询,例如:“在过去一周的访问中返回租户6中以‘/blog’开头的所有页面。”使用常规PostgreSQL表如果我们的数据驻留在单个PostgreSQL节点中,我们可以使用SQL提供的丰富的关系操作集轻松地表达我们的查询:SELECTpage_id,count(event_id)FROMpageLEFTJOIN(SELECT*FROMeventWHERE(payload->>'time')::timestamptz>=now()-interval'1week')recentUSING(tenant_id,page_id)WHEREtenant_id=6ANDpathLIKE'/blog%'GROUPBYpage_id;只要此查询的工作集适合内存,这就是适合许多应用程序的解决方案,因为它提供了最大的灵活性。然而,即使您还不需要扩展,考虑扩展数据模型的影响也是有用的。按ID分布表随着租户数量和每个租户存储的数据的增长,查询时间通常会随着工作集不再适合内存或CPU成为瓶颈而增加。在这种情况下,我们可以使用Citus跨多个节点分片数据。分片时我们需要做的第一个也是最重要的选择是分布列。让我们从简单的选择开始,使用事件表的event_id和页表的page_id:--简单地使用event_id和page_id作为分布列SELECTcreate_distributed_table('event','event_id');选择create_distributed_table('page','page_id');鉴于数据分布在不同的worker中,我们不能像在单个PostgreSQL节点上那样简单地执行连接。相反,我们需要发出两个查询:跨页表的所有分片(Q1):SELECTpage_idFROMpageWHEREpathLIKE'/blog%'ANDtenant_id=6;跨事件表的所有分片(Q2):SELECTpage_id,count(*)AScountFROMeventWHEREpage_idIN(/*…pageIDsfromfirstquery…*/)ANDtenant_id=6AND(payload->>'time')::date>=now()-interval'1week'GROUPBYpage_idORDERBYcountDESCLIMIT10;之后,应用程序需要结合这两个步骤的结果。回答查询所需的数据分散在不同节点上的分片中,每个分片都需要查询:在这种情况下,数据分布产生了一个很大的缺点:查询每个分片的开销,运行多个查询的开销Q1返回客户端Q2的许多行变得非常大,需要分多个步骤编写查询,合并结果,并要求以这种方式更改应用程序。但是,这仅在查询工作量远大于查询许多分片的开销时才有用。通常最好避免直接从应用程序执行此类繁重的工作,例如通过预聚合数据。再次查看我们对按租户表分布的查询,我们可以看到查询所需的所有行都有一个共同的维度:tenant_id。仪表板只会查询租户自己的数据。这意味着如果同一个租户的数据总是在一个PostgreSQL节点上,那么我们的原始查询可以通过在tenant_id和page_id上执行连接来由该节点一次性回答。在Citus中,具有相同分布列值的行保证在同一个节点上。分布式表中的每个分片实际上都有一组来自其他分布式表的位于同一位置的分片,这些分片包含相同的分布列值(来自同一租户的数据)。从头开始,我们可以创建一个以tenant_id作为分布列的表。--通过使用公共分布列来共同定位表SELECTcreate_distributed_table('event','tenant_id');SELECTcreate_distributed_table('page','tenant_id',colocate_with=>'event');在这种情况下,Citus可以回答您在单个PostgreSQL节点上运行的相同查询,无需修改(Q1):SELECTpage_id,count(event_id)FROMpageLEFTJOIN(SELECT*FROMeventWHERE(payload->>'time')::timestamptz>=now()-interval'1week')recentUSING(tenant_id,page_id)WHEREtenant_id=6ANDpathLIKE'/blog%'GROUPBYpage_id;多亏了tenantid过滤器和tenantid上的连接,Citus知道可以将特定租户包含在同一位置的一组数据分片来回答整个查询,而PostgreSQL节点可以一步回答该查询,启用完整的SQL支持。在某些情况下,需要稍微修改查询和表架构以确保tenant_id始终包含在唯一约束和连接条件中。然而,这通常是一个简单的更改,并且避免了在没有托管的情况下所需的大量重写。由于特定的tenant_id=6过滤器,上面的示例仅查询一个节点,尽管存在SQL限制,但托管还允许我们在所有节点上高效地对tenant_id执行分布式连接。共存意味着更好的特性支持Citus通过共存解锁的完整特性列表如下:对一组共存分片查询的完整SQL支持一组共存分片的多语句事务支持修改聚合通过INSERT..SELECT分布式外连接下推CTE的外键(需要PostgreSQL>=12)数据托管是一种强大的技术,它提供水平扩展和对关系数据模型的支持。使用分布式数据库(通过协同定位进行关系操作)迁移或构建应用程序的成本通常远低于迁移到限制性数据模型(如NoSQL)的成本,并且与单节点数据库不同,它随大小而扩展,而不是规模您的业??务水平。有关迁移现有数据库的更多信息,请参阅过渡到多租户数据模型。查询性能Citus通过将传入查询分解为在工作分片上并行运行的多个片段查询(“任务”)来并行化传入查询。这允许Citus为每个查询利用集群中所有节点的处理能力,以及每个节点上单个内核的处理能力。由于这种并行化,您可以获得集群中所有核心的计算能力的累积性能,与单个服务器上的PostgreSQL相比,查询时间显着减少。Citus在规划SQL查询时采用两阶段优化器。第一阶段涉及将SQL查询转换为它们的交换和连接形式,以便它们可以被下推并在工作线程上并行运行。如前几节所述,选择正确的分布列和分布方法允许分布式查询计划器对查询应用各种优化。由于网络I/O减少,这会对查询性能产生重大影响。Citus的分布式执行器然后将这些单独的查询片段发送到PostgreSQL工作实例。分布式规划器和执行器都有几个可以调整以提高性能的方面。当这些单独的查询片段被发送给工作人员时,查询优化的第二阶段就开始了。Workers只是运行扩展的PostgreSQL服务器,他们应用PostgreSQL的标准规划和执行逻辑来运行这些碎片化的SQL查询。因此,任何有助于PostgreSQL的优化也有助于Citus。PostgreSQL默认带有保守的资源设置;因此优化这些配置设置可以显着缩短查询时间。我们在文档的查询性能调整部分讨论了相关的性能调整步骤。