- 1 Citus 简介
- 2 四种典型的应用场景及其需求
- 3 Citus 架构
- 4 Citus 是如何修改 PG 本身的行为的
- 5 Citus 的整体架构
- 6 Citus 的表类型
- 7 数据再平衡
- 8 分布式查询计划器
- 9 分布式查询执行器
- 10 分布式事务
- 11 其他场景的分布式处理
- 12 高可用和备份
- 13 性能测试
- 14 应用案例
- 15 相关工作
本文为摘录,原文为: https://zhuanlan.zhihu.com/p/667067679
1 Citus 简介
作为非常著名的开源数据库,PostgreSQL 在许多商业场景得到应用,促使将其扩展为多机器的分布式数据库。Citus 基于 PostgreSQL 的 插件接口,构建了分片层、分布式查询计划生成器、执行器和分布式事务,保留了 PG 的各种功能。文章基于 Citus 应用,总结了四种需要 扩展 PG 的场景和对分布式数据库的功能需求。接着,介绍了 Citus 如何基于 PG 的扩展接口构建分布式数据库系统,并解决各类应用场景的 需求。
2 四种典型的应用场景及其需求
扩展 PG 的四种应用场景:
- 多租户、
- 实时分析、
- 高性能增删改查、
- 数据仓库。
2.1 多租户
多租户的场景是一个很典型的需要多数据进行分片的场景,
- 最传统的方式是对租户的数据进行手动分片,将不同租户的数据放到不同的分片下,以实现资源的隔离。(我们的数据库就是这种方式)。
- 还有一种方式是通过 tenant ID 列来对数据进行分片,Citus 采用的就是这种方式, 这种方式下,往往需要不同租户采用相同的数据表模式,但事实上用户会希望针对不同租户设置特定的数据表模式。
针对这一需求,作者简易可以使用 JSONB 数据类型累进行扩展。 同时,客户也需要能够控制租户数据的位置,这样可以在监控到热点的时候对数据进行迁移。
2.2 实时分析
- 实时分析的目的是实时处理大规模流数据,主要涉及 event 数据和时间序列数据,用于系统监控和异常检测等。
- 数据库需要高写吞吐和每秒数百个读请求处理能力,在这种场景下,查询通常基于索引、物化视图和数据转换请求,需高效地更新数据。
- PG 通过 COPY 语句快速实现数据导入,MVCC 支持同时写入和服务读请求,也支持复杂数据类型,但单机存储限制是其缺点。
为解决单机存储限制,数据库系统需要数据分发到不同机器,并支持并行批量加载数据。Citus 可实现分库分表,分发写入的数据,后续 将介绍 Citus 如何支持高效查询,包括跨节点 join 等操作。
2.3 高性能增删改查
- 高性能的 CRUD (创建、读取、更新、删除) 操作对于处理大量相对独立的对象或文档至关重要。
- 应用程序通常会简单地执行 CRUD 操作,但也可能需要进行复杂的查询, 这些对象通常采用类似 JSON 的非结构化数据格式。
- PostgreSQL 支持针对 JSON 的查询,一台大型的 PostgreSQL 服务器可以处理每秒数十万次的写入操作和数百万次的读取操作。
- 然而,PostgreSQL 的 MVCC(多版本并发控制)模型需要写入新副本,并通过自动清理来回收空间。
- 如果自动清理不能及时跟上,则会影响性能。
- 此外,由于 PostgreSQL 采用每个连接一个进程的架构,以及进程的相对高内存开销,它只能处理有限数量的空闲连接。
2.4 数据仓库
- 数据仓库将不同来源的数据合并到单个数据库系统,生成即时分析报告。
- 该应用通常不需要低延迟或高吞吐量,但查询可能需要扫描大量数据。
- 数据库需支持快速扫描,为手写 SQL 找到高效的查询计划。
PostgreSQL 在执行大扫描时存在扫描性能和并行性方面的限制,但其性能特性和全面的 SQL 支持使其成为分析吸引人的选择。
为扩展数据仓库应用,可通过并行、分布式 SELECT 和列式存储加速扫描。应选择分布列以增加共同分布连接数量,同时支持非共同分 布连接。查询优化器需最小化网络流量的连接顺序。
3 Citus 架构
在 Citus 集群中,所有服务器都运行带有 Citus 扩展以及任意数量其他扩展的 PG 实例。Citus 使用 PG 扩展 API 来以两种方式改变 数据库的行为:
- 首先,Citus 将数据库对象(如自定义类型和函数)复制到所有服务器。
- 其次,Citus 添加了两种新的表类型来利用外部服务器。
4 Citus 是如何修改 PG 本身的行为的
PG 本身提供大量的编程接口,我们可以在扩展插件中编写自己的方法来调整数据库的行为。 Citus 主要使用了以下几个接口:
User-defined functions (UDFs)
可以在 SQL 查询时作为事务一部分被调用,主要用于操作 Citus 元数据以及执行 RPC。查询计划器和执行器的 hook
本身是一组全局函数指针,允许扩展提供替代的查询计划和执行方法。在 PG 解析查询后,Citus 会检查查询是否涉及 Citus 表。如 果是的话,Citus 会生成一个包含 CustomScan 节点的计划树,该节点封装了分布式查询计划。CustomScan
是 PG 查询计划中的执行节点,它保存自定义状态并通过自定义函数指针返回元组。Citus CustomScan 调用分布式查询执行器,该执 行器将查询发送到其他服务器,并在将结果返回给 PG 执行器之前收集这些结果。Transaction callbacks
在事务的生命周期中的关键点(例如预提交、后提交、中止)被调用。Citus 使用这些回调来实现分布式事务。Utility hook
在解析不经过常规查询计划器的任何命令后被调用。Citus 用这些 hook 来执行 Citus 表相关的 DDL 和 COPY 命令。Background workers
在单独的进程中运行用户提供的代码。Citus 使用此 API 运行维护守护进程。该守护进程执行分布式死锁检测、两阶段提交准备事务 恢复和清理。
通过这些钩子,Citus 可以拦截客户端和涉及 Citus 表的 PG 引擎之间的任何交互,并替换或增强 PG 的行为。
5 Citus 的整体架构
Citus 部署通常包括一个 coordinator 和多个 worker.
- coordinator 保存表的元数据,用于客户端连接使用。 当用户通过 Citus UDF 添加工作节点时,PG 服务器隐式成为协调器。
- worker 存储数据分片。
集群规模小时,协调器也可用作工作节点,最小 Citus 集群是单服务器。
单个 coordinator 作为入口点好处是 PG 库和工具可与 Citus 集群交互,就好像它是一个普通的 PG 服务器。 由于分布式查询的开销与查询执行相比较小,一个大型协调器节点可以处理每秒数十万次的事务或通过 PG 的 COPY 命令每秒摄入数百万行的数据。
coordinator 节点可能成为系统瓶颈,Citus 通过元数据分发到 worker 节点解决,降低 coordinator 查询压力。 coordinator 只负 DDL 执行,减少压力。但这种方案可能导致 client 连接集群创建更多连接,可能带来另一个瓶颈。
6 Citus 的表类型
Citus 有两种表, 分布表 和 引用表 。
在创建本地表后,通过执行 Citus 方法可将其转换为 Citus 表,并由 Citus 接管所有相关操作。 Citus 利用真分区键进行哈希,将数据 均匀分布到 worker 节点上。 Citus 可以保证,相同哈希值的数据在相同节点,避免跨节点通信。 分布表数据哈希分片到 worker 节点,引用表所有节点复制同步。 在 join 时,worker 只需对本地分片执行。
7 数据再平衡
当单个 worker 节点的数据达到负载极限时,我们需要将数据移动到新的节点,从而实现整体的负载均衡。
Citus 提供一个 rebalancer 来执行移动数据的操作,在执行 rebalance 操作时,rebalancer 会选取一个分片和与之相关的其他数据, 通过 PG 的逻辑复制进行移动,这时,分片依然可以接收读写请求。
在完成所有存量和增量的复制后,Citus 会对分片加上写锁来等待所有复制完成,并执行分布表的元数据更改。这时一般会有几秒的写宕 机。
8 分布式查询计划器
在客户端请求查询 Citus 相关的数据表时,Citus 会生成一个包含有分布式查询计划的 Custom-Scan 节点,Citus 有不同的查询计划器, 以应对不同场景下的查询请求。如下图所示:
(A): Fast path planner
- 快速路径规划器处理的是针对单个表的简单 CRUD 查询,并且该表只有一个分布列的取值。
- 它直接从查询中的过滤条件中提取分布列的值, 并确定与该值匹配的分片。
- 然后,规划器将表名重写为分片名,以构建在工作节点上运行的查询,这可以在 CPU 开销极小的情况下完成。
- 因此,快速路径规划器支持高吞吐量的 CRUD 工作负载。
(B): Router planner
- 路由规划器处理的是可以限定在一组共同分布的分片上的任意复杂查询。
- 路由规划器会检查或推断所有分布表是否具有相同的分布列过滤条件。
- 如果是这样,查询中的表名将被重写为与分布列值匹配的共同分布分片的名称。
- 路由规划器隐式支持 PostgreSQL 支持的所有 SQL 特性,因为它会将完整的查询委托给另一个 PostgreSQL 服务器。
- 因此,路由规划器使多租户 SaaS 应用能够在最小开销下使用所有 SQL 特性。
(C): Logical planner
- 逻辑规划器通过构建多关系代数树来处理跨分片的查询。
- 多关系代数形式化了两个在 PostgreSQL 中不可用的分布式执行原语,用于收集和重新分区数据。
- 这种差异影响了路由规划器和逻辑规划器之间的分离。
- 逻辑规划器的目标是在结果在 coordinator 上合并之前,将尽可能多的计算推送到工作节点。
这里有两种不同的逻辑规划策略:
逻辑下推规划器:
- 检测连接树是否可以完全下推。
- 这要求所有分布表之间具有共同分布的连接,并且子查询不需要全局合并步骤(例如,GROUP BY 必须包含分布列)。
- 如果是这样,规划器可以基本上不关心连接树中使用的 SQL 构造,因为它们完全委托给工作节点,分布式查询计划变得非常容易并 行化。
逻辑连接顺序规划器:
- 确定涉及非共同分布连接的连接树的最佳执行顺序。
- 它使用共同分布连接、广播连接和重新分区连接来评估分布表和子查询之间所有可能的连接顺序,并选择最小化网络流量的顺序。
- 广播连接和重新分区连接会导致带有过滤器和投影的子计划被推送到子计划中。
对于每个查询,Citus 会按照最低到最高开销的顺序遍历这四个规划器。如果某个特定规划器可以为查询生成计划,Citus 就会使用它。 在特定场景下,相比执行而言,查询计划的生成在时间开销上很低。
9 分布式查询执行器
PG 的查询计划是一个由多个执行节点组成的执行树,每个节点都有一个返回一个元组的函数。 Citus 生成的 CustomScan 就是其中的一个节点。
PG 的执行器进入 CustomScan 函数后,Citus 会执行各个子计划,然后把执行移交给 adaptive executor 自适应执行器。 自适应执行器可以通过单工作节点多路连接的方式并行执行查询任务。 这种多路实现的方式需要管理过程中建立连接,以及并行处理过程中的额外开销。
自适应执行器需要权衡并行执行的延迟与各类开销。 这里提出了一个“ slow start ”慢启动的方案。 查询开始时,执行器对每个工作节点只建立一个连接,接下来,每 10ms,执行器会给每个工作节点的连接数(n)加 1。 如果有 t 个等待交给某个工作节点执行的任务没有被分配到可用的连接, 那么执行器就会为这个工作节点创建 min(n, t) 个连接,并放入连接池中。
这种方案的原因在于,一个简单的内存中基于索引的查询往往只需要不到 1ms 的时间,所以一般来说节点上的所有任务会在执行器尝试打开连接之前完成。 此外,分析型任务往往需要数百毫秒,尽管连接建立有一定的延迟,但是在整体的时间开销里几乎可以忽略。 当然,这种方案下,执行器仍然需要管理与各个节点的连接数。
在执行连接上的任务分配时,由于每个连接到分片上执行查询时访问的是不同的数据,并在多语句事务的情况下保持未提交的写入和锁定。 因此,对于每个连接,Citus 会跟踪已访问的分片,以确保相同的连接将在同一事务中对同一组共同分布的分片进行任何后续访问。 在开始执行语句时,如果在事务中已经访问了分片,则将任务分配给对应的连接,否则将其分配给工作节点的通用池。当连接准备就绪时,执 行器首先从其队列中获取一个已分配的任务,否则从通用池中获取任务。
通过结合慢启动、共享连接限制和任务分配算法,自适应执行器可以处理各种工作负载模式,即使它们在单个数据库集群上并发运行,并 支持复杂的交互式事务块而不损失并行性。
10 分布式事务
Citus 中的事务主要分两种,一种是在 coordinator 上的事务,一种是在工作节点上的事务。
- 对于仅仅涉及单个工作节点的事务,工作节点全权负责整个事务;
- 对于涉及多个节点的事务则通过 两阶段提交 来保证原子性。
单节点事务其实比较简单,我们主要看看多节点的事务如何基于两阶段提交来实现。
对于涉及多个节点的写事务,执行器在工作节点上开启事务块,并在提交时对它们执行两阶段提交(2PC)。 PostgreSQL 实现了准备事务状态的命令,以一种保留锁并在重新启动和恢复时保留状态的方式。 这使得稍后可以提交或中止已准备好的事务。Citus 使用这些命令来实现完整的 2PC 协议。
当协调器上的事务即将提交时,预提交回调通过所有与开启事务块的工作节点的连接发送“准备事务”命令。
- 如果成功,协调器为每个已准备好的事务在 Citus 元数据中写入一个提交记录,然后本地事务提交,确保提交记录被耐久存储。
- 在提交后和中止回调中,已准备好的事务将尽力提交或中止。
当一个或多个已准备好的事务无法提交或中止时,将使用 Citus 元数据中的提交记录确定事务的结果。
- 后台守护进程定期比较每个工作节点上待处理的准备好的事务列表和本地的提交记录。
- 如果存在已准备好的事务的提交记录(即:可见),则协调器已经提交,因此已准备好的事务也必须提交。
- 当存在多个协调器时,每个协调器为其启动的事务执行 2PC 恢复。
- 反之,如果一个已结束的事务没有记录存在,那么已准备好的事务必须中止。
- 由于提交记录和已准备好的事务都存储在预写式日志中,这种方法对涉及的任何节点的故障是强大的。
- 后台守护进程定期比较每个工作节点上待处理的准备好的事务列表和本地的提交记录。
另一个关键点在于 如何处理分布式死锁 ,特别是在多语句事务之间。为了解决这个问题,可以使用死锁预防或死锁检测方法。 死锁预防技术(例如 Wound-Wait)需要一定百分比的事务重新启动。 PostgreSQL 具有交互式协议,这意味着在重新启动发生之前可能将结果返回给客户端,而且不希望客户端重试事务。 因此,Wound-Wait 对于 Citus 来说不太适用。 为了保持与 PostgreSQL 的兼容性, Citus 实现了分布式死锁检测 ,当事务陷入实际死锁时,会中止事务。
PostgreSQL 已经在单节点上提供了死锁检测。Citus 通过在协调器节点上运行的后台守护程序扩展了这一逻辑。 该守护程序:
- 每 2 秒轮询所有工作节点,以获取其锁图中的边缘(进程 a 等待进程 b),
- 然后合并在同一分布式事务中参与的图中的所有进程。
- 如果生成的图包含一个环路,那么将向属于环路中最年轻的分布式事务的进程发送取消命令,以中止事务。
除非存在实际死锁,否则在典型的(分布式)数据库工作负载中,只有少数事务会在等待锁。因此,分布式死锁检测的开销很小。 当分布式死锁经常发生时,建议用户更改其事务中语句的顺序。
Citus 中的多节点事务提供了原子性、一致性和持久性的保证,但 不提供分布式快照隔离的保证 。 并发的多节点查询可能在一个节点上提交之前获取本地 MVCC 快照,而在另一个节点上提交之后获取快照。 解决这个问题需要对 PostgreSQL 进行更改,使快照管理器可扩展。 在实践中,我们在这四种工作负载模式中并没有发现对分布式快照隔离的强烈需求,客户目前也没有表达对此的需求。 在多租户和 CRUD 应用程序中,大多数事务范围仅限于单个节点,这意味着它们在该节点上获得了隔离保证。 分析应用程序之间的事务没有强依赖关系,因此对宽松的保证更具宽容性。
在某些混合场景中,分布式快照隔离可能很重要。 然而,现有的分布式快照隔离技术由于需要额外的网络往返或等待时钟而具有显著的性能成本,这会增加响应时间并降低可实现的吞吐量。 在同步的 PostgreSQL 协议的背景下,吞吐量最终受到#连接数/响应时间的限制。 由于从应用程序的角度来看,建立大量的数据库连接通常是不切实际的,因此低响应时间是实现高吞吐量的唯一途径。 因此,如果将来实施分布式快照隔离,我们可能会将其作为可选项。
11 其他场景的分布式处理
除了简单的 SELECT 语句和其他 DML 命令外,Citus 还提供其他语句的支持。
DDL 命令
- 在 PG 里面是在线处理的具有事务性的操作,
- Citus 同样通过加锁来保持相同的特性,并通过执行器将命令发送到 worker 节点
COPY 命令
- 在 PG 里面可以被用来导入 CSV 格式的数据,这个过程在 PG 里面是单线程的实现,并需要更新索引、检查各类约束条件。
- 在 citus 里面,coordinator 会在每一个分片和数据流上异步开启 COPY 命令,这样写操作也可以并行执行。
跨分布表的 INSERT…SELECT 命令往往采用以下三种步骤之一去执行:
- 如果 SELECT 操作在协调器上需要执行合并步骤,则该命令在内部作为分布式 SELECT 执行,然后将结果 COPY 到目标表中。
- 如果没有合并步骤,但源表和目标表不是共位的,则 INSERT..SELECT 在将 SELECT 结果插入目标表之前执行分布式重新分区。
- 如果源表和目标表是共位的,则 INSERT..SELECT 直接在并行的共位分片上执行。
在 Citus 中,存储过程可以基于分布参数和一个共位的分布表被委托给工作节点,以避免协调器和工作节点之间的网络往返。 工作节点可以在不进行网络往返的情况下在本地执行大多数操作,但在必要时也可以在工作节点之间执行分布式事务。 这种方法有助于在分布式环境中优化存储过程的性能。
12 高可用和备份
12.1 高可用
- 在 Citus 中,HA 主要在服务器层使用现有的 PostgreSQL 复制进行处理。
- 在 HA 设置中,集群中的每个节点都有一个或多个热备份节点,并使用同步、异步或 quorum 来复制其写前日志(WAL)。
- 当一个节点失败时,集群协调器会提升一个备用节点,并更新 Citus 元数据、DNS 记录或虚拟 IP。
整个故障切换过程需要 20-30 秒,在此期间涉及该节点的分布式事务会回滚。
Coordinator 通常是托管服务中的控制面的一部分,但本地用户可以使用 pg_auto_failover
扩展来执行相同的功能。
12.2 备份
备份也主要在服务器级别进行,通过创建周期性的磁盘快照或数据库目录的副本,并在每个服务器中将 WAL 持续存档到远程存储来实现。
Citus 支持定期创建一致的还原点,即每个节点的 WAL 记录。
- 还原点是在将写操作阻塞到 coordinator 上的提交记录表时创建的,这可以防止在创建还原点时进行中的两阶段提交。
将所有服务器还原到相同的还原点可以保证在恢复的集群中,所有多节点事务要么完全提交要么中止,或者可以通过协调器在启动时执行 2PC 恢复来完成。
13 性能测试
Benchmark 部分,作者的实验基本围绕着不同场景下 PG 单机和 Citus 不同部署模型下的性能差距,包括 latency 、 QPS 、 TPS 等等。
14 应用案例
这部分介绍了 Citus 在微软内部的使用,这一场景主要是一个数据分析场景,数据来自全球数亿台 windows 设备,指标显示在一个名为 “Release Quality View”(RQV)的实时分析仪表板上,该仪表板帮助 Windows 工程团队评估每个 Windows 版本的客户体验质量。
RQV 的底层数据存储,代号为 VeniceDB,由两个在 Microsoft Azure 上运行的超过 1000 核心的 Citus 集群提供支持,存储了超过一 PB 的数据。虽然对于 VeniceDB 评估了许多不同的分布式数据库和数据处理系统,但只有 Citus 能够满足与 PB 级 VeniceDB 工作负载 相关的特定要求,包括:
- 对于每天超过 6 百万次查询,p95 下的小于一秒的响应时间
- 每天约 10TB 的新数据
- 在 RQV 中显示新的数据需要在 20 分钟内完成
- 具有高基数 group by 的嵌套子查询
- 高级二级索引(例如部分索引、GiST 索引)以高效查找沿各个维度的报告
- 高级数据类型(例如数组、HyperLogLog)以在 SQL 中实现复杂的分析算法
- 通过增量聚合减少行数
- 在节点间进行原子更新以清理错误数据
在 Citus 集群中,原始数据存储在名为 measures 的表中,该表按设备 ID 进行分布,并使用 PostgreSQL 中内置的分区功能按时间进行磁盘分区。 使用 COPY 命令来并行化将传入的 JSON 数据导入分布式表。使用分布式 INSERT..SELECT 命令来执行设备级别的对传入数据进行预聚合,并将 其放入几个具有不同索引的 reports 表中。reports 表也按设备 ID 进行分布,并与 measures 表共位,以便 Citus 可以完全并行化 INSERT..SELECT。
这里给出了一个典型的查询语句:
SELECT ..., avg(device_avg)
FROM (
SELECT deviceid, ..., avg(metric) as device_avg
FROM reports WHERE ...
GROUP BY deviceid, <time period> , <other dimensions> ) AS subq
GROUP BY <time period>, <other dimensions>;
这些查询通过多个维度进行过滤(例如测量、时间范围、Windows 版本),以找到数据的重要子集。嵌套的子查询首先通过设备 ID 对报 告进行聚合,这对于按设备而不是按报告数量来衡量整体平均值是必要的。每个查询可能涉及数千万台设备,这使得按 deviceid 进行 GROUP BY 的计算变得具有挑战性。由于子查询按照分布列进行分组,Citus 中的逻辑推送计划器认识到它可以将整个子查询推送到所有工 作节点以进行并行化。然后,工作节点使用仅索引扫描按设备 ID 顺序读取数据,并最小化 GROUP BY 的磁盘 I/O 和内存占用。最后, Citus 通过在工作节点上计算部分聚合并在协调器上合并这些部分聚合来分发外部聚合步骤,以生成最终结果。
15 相关工作
这部分大体介绍了当前市面上常见的分布式数据库解决方案:
- 针对 MySQL 的类似于 Citus 的方案 Vitess,采取了和 Citus 相似的实现方案 - 基于 PG 的解决方案 Greenplum and Redshift,相比
Citus 而言对分析场景具有更好的支持,比如采用列存来实现快速 scan,通过数据 shuffle 来优化 join 性能等
- Aurora 同样也对 PG 进行了支持,通过分布式存储的实现,Aurora 采用了存算分离、共享存储的方案,这种方案的好处在于调用端不需
要做许多分布式场景下的决策,可以直接把 Aurora 当做单机 DB 来使用。 Citus 则需要调用方对分布方案有足够的理解和干预。
- Spanner ,CockroachDB 和 Yugabyte 主要面向需要分布式事务支持的场景。CockroachDB 和 Yugabyte 也部分支持 PostgreSQL 协议。
与 Citus 相比,这些系统的一个显著的架构差异在于它们提供了分布式快照隔离,并使用了"等待-等待"(wound-wait)而不是死锁检测。 分布式快照隔离的一个优点是它避免了数据建模的约束。Citus 用户需要使用邻近数据存储和引用表,以将事务范围限制到单个节点,以 获得完整的 ACID 保证。另一方面,这些技术还能实现高效的连接和外键,因此它们对于扩展复杂的关系数据库工作负载是至关重要的。
- TimescaleDB 是一个为时间序列数据优化的 PostgreSQL 扩展。它使用与 Citus 相似的钩子来引入“超级表”(hypertable)的概念,该
表会根据时间自动进行分区。按时间对表进行分区对于限制索引大小以保持时间序列工作负载的高写入性能,以及通过时间范围进行分区 修剪以加速查询是有用的。由于对 PostgreSQL 钩子的冲突使用,目前 Citus 和 TimescaleDB 不兼容,但 Citus 可以与 pg_partman 一 起使用,后者是一个更简单的时间分区扩展。许多使用 Citus 的实时分析应用程序也会在分布式表的基础上使用 pg_partman,在这种情 况下,各个分片会被本地分区,以获得分布式表和时间分区的双重优势。
整体而言,Citus 的分布式解决方案需要用户直接介入数据切片、数据同步、数据存储等多个过程,对于使用者而言需要一定背景知识。 这一实现方案的好处在于 Citus 可以快速发布支持最新版本 PG 的新版本。