PostgreSQL 18 下 Citus 分布式集群的搭建与实战经验
2026/9/12 13:11:04 网站建设 项目流程

PostgreSQL 18 正式发布以后,后台问得最多的问题之一就是:“Citus 能不能跑在 18 上?分布式这块实战起来到底靠不靠谱?”说实话,Citus 这几个词在国内 PG 圈子里热度从来没低过——一个开源扩展,能把单机 PostgreSQL 变成一套具备分片、并行查询、在线重平衡能力的分布式集群,听起来确实香。但真正上手过的朋友都知道,Citus 不是“装个扩展、建几张分布表”那么简单,分布列选不好、分片规划不对、版本没对齐,后期一堆坑等着你。

这篇就顺着《PostgreSQL 18 从新手到大师:实战指南》第 4.6 节的思路,把我实际部署、迁移、压测过程中攒下的经验完整拆开讲一遍。文章适合刚接触分布式数据库的 PG 用户,也适合已经在用 Citus、但想优化集群架构的 DBA。看完能理解 Citus 的底层运作方式,也能直接照着我给的步骤搭出一套可用的分布式 PostgreSQL 集群。

1. 先说结论:为什么单库扛不住,Citus 到底解决了什么

很多人第一反应是“我的 PostgreSQL 现在跑得好好的,为什么要上分布式”。这个问题的答案,等你某一天打开监控面板看到这几条曲线就会明白了:CPU 长期 80% 以上、磁盘 IO 排队、慢查询日志里全是超过 2 秒的 SQL。单机 PostgreSQL 的写入和查询能力确实强,但瓶颈是物理资源,到了一定规模之后,再怎么调参数、加索引都是治标不治本。

1.1 单库的天花板:一张订单表从 100GB 开始失控

我经手过一套典型的业务系统,订单表三年涨到了 180GB,单表行数超过 4 亿。刚开始还能靠分区表按月份切,勉强压住了查询范围;可是促销季一来,用户维度的实时查询全落到大分区上,索引再精简也无济于事。另一个更痛的点是写入:单机主库的 WAL 写入瓶颈摆在那里,备库延迟经常跑到几十秒,DBA 半夜被叫起来处理主备切换报警,属于家常便饭。

这种时候你会面临几个选择:继续砸钱升硬件、换商业分布式数据库,或者用开源方案做水平扩展。如果你不想把原有 PostgreSQL 生态扔掉,不想把 SQL 全部重写,Citus 就是最平滑的一条路。它不是一个全新的数据库,而是 PostgreSQL 的一个扩展。你原本的 psql、JDBC、ORM、备份工具全部照常使用,只是多了一层分布式的“外挂”。

1.2 Citus 的定位:给 PostgreSQL 插上分片的翅膀

Citus 本质上是把单张表按某个列的取值拆成多个分片(shard),然后分散放置到不同的工作节点上。查询进来的时候,协调器节点负责解析 SQL、把能下推的部分下推到各个工作节点并行执行,最后汇总结果返回给客户端。

这个设计带来的最大好处是“对应用透明”。应用层看到的还是一张完整的逻辑表,写普通 SQL 就行;Citus 在内部帮你完成了路由、并行、聚合、合并这些脏活。对比其他分布式中间件方案,比如在业务代码里做分库分表、或者用 ShardingSphere 这类代理层,Citus 的做法更贴近数据库本身,Function Pushdown 和 JOIN Pushdown 的支持也更完整。当然,它也有自己的限制,比如跨分片事务有额外开销、分布式表的主键必须包含分布列,这些后面我会专门讲。

2. 架构和工作原理:协调器、工作节点和分片到底怎么配合

Citus 集群里主要有两类角色:协调器和工作节点。把想象场景切换到公司组织,协调器是“项目经理”,工作节点是“一线开发”,分片是“具体任务”。项目经理不负责具体实现,只负责拆任务、派活、把各人的产出汇总起来;一线开发只管把自己领到的任务做完。

2.1 三角色模型:协调器、工作节点、分片

协调器节点(Coordinator)保存整个集群的元数据,包括哪张表分了几个分片、每个分片放在哪个节点上、分布列用的是哪个字段。它同时对外提供统一的数据库连接入口,你平时连的就是这个节点。严格来说协调器本身也存储数据,它上面的元数据表以及引用表的数据都真实存在,所以部署时候要考虑它的容灾,不能当成纯粹的无状态代理。

工作节点(Worker)负责实际存储分片、执行下推过来的查询,在 Citus 的术语体系里叫 shard。每个分片对应 PostgreSQL 里一张真实存在的物理表,命名模式是“表名_分片编号”。比如 orders 表有 32 个分片,工作节点上会看到 orders_102008、orders_102009 这样的物理表。分片之间默认是独立的,可以设置副本数(shard replication factor),副本会按照故障域自动分布到不同物理节点上。

还有一个角色是引用表(Reference Table),它不分片,而是在每个工作节点上都放一份完整副本。这种表适合放数据量不大、但频繁参与 JOIN 的维度表,比如地区表、商品分类表。因为每个节点都有完整数据,JOIN 的时候可以直接本地关联,不用跨节点拉数据。

2.2 查询是怎么被拆下去的

理解 Citus 的查询执行方式是用好它的关键。你发送一条“select sum(amount) from orders where user_id = 88”,协调器先根据分布列 user_id 判断 88 落在哪个分片,然后只把查询发给那个分片所在的工作节点。这种叫“单分片查询”,性能最好,是分布式系统里的理想情况。

如果查询条件不带分布列,比如“select count(*) from orders where create_time > now() - interval '7 days'”,协调器无法定位到单个分片,只能把查询广播到所有分片,每个分片各自算出局部结果,再由协调器合并成最终结果。这种叫“分布式查询”或者说“广播查询”,整体耗时取决于最慢的那个分片。所以“分布列选得好不好”直接决定了绝大多数查询是单分片还是全集群扫描。

值得一提的是,Citus 对 JOIN 做了大量优化。如果两张分布表用的是同一个分布列,它们可以在本地直接做 JOIN,数据不用挪来挪去;如果分布列不同,Citus 会尝试把被驱动表的数据按驱动表的分布列进行重分布,这个过程有网络传输开销,但 SQL 语义不会错。

2.3 分布式事务和一致性保证

Citus 从 10 版本开始引入了内置的分布式事务管理,基于 PostgreSQL 的两阶段提交协议(2PC)实现跨分片的写入一致性。以前版本跨分片事务还常有“部分成功、部分失败”的说法,现在只要配置正确,Citus 可以保证分布式事务的原子性。

但要注意,事务开销和分片数量直接相关。单条 INSERT 只会落在一个分片,事务开销很小;一次 UPDATE 如果影响跨多个分片,协调器就需要和工作节点多次协调,延迟明显上涨。所以在设计模型时,尽量让事务操作集中在同一个分片内,也就是让事务里的数据行尽量落到同一个分布列取值上。这一点和分库分表中间件的“同库同表”思想是一样的。

3. 环境准备与安装部署:在 PostgreSQL 18 上把 Citus 跑起来

把架构看明白了,就可以动手了。部署这一步我踩过不少版本兼容的坑,这里把最稳妥的路径写出来。

3.1 版本匹配先搞清楚

Citus 的版本号是独立发布的,发布时间一般会落后 PostgreSQL 主版本几个月。PostgreSQL 18 发布之后,我们需要确认对应的 Citus 版本是否已经支持。从目前官方支持的平台矩阵来看,Citus 通常会在 PG 新版本发布后一两个季度内提供适配包。安装之前,一定要先上 citusdata.com 或者 PGDG(PostgreSQL Global Development Group)仓库里确认一下有没有对应的 citus_18 包。

我曾经在生产环境犯过一个错:PG 17 发布了,但那次用的 Citus 版本只支持到 PG 16,我当时没仔细看包名,直接用 yum update 把 PostgreSQL 升到 17,结果数据库起来之后扩展列表里压根找不到 citus,更惨的是元数据表 pg_dist_node 相关的视图全不见了。那一次教训让我记住了:Citus 包名和 PostgreSQL 主版本号是强绑定的,比如 RHEL 系下安装包叫 citus_18,Debian/Ubuntu 系叫 postgresql-18-citus,装错版本等于摆设。

3.2 RHEL 系安装步骤

我平时测试集群多用 RHEL 系的虚拟机,CentOS 9 或者 Rocky Linux 9,步骤可以照抄。先把 PGDG 仓库配好:

yum install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %rhel)-x86_64/pgdg-redhat-repo-latest.noarch.rpm

然后安装 PostgreSQL 18 的服务端和 Citus 扩展:

yum install -y postgresql18-server postgresql18-contrib citus_18

初始化数据库集群并启动服务:

/usr/pgsql-18/bin/postgresql-18-setup initdb systemctl enable --now postgresql-18

这一步完成后,用 postgres 用户登录,先改密码,再修改监听配置。注意协调器和每个工作节点都要执行类似操作,并且要在 pg_hba.conf 里放行集群内部节点之间的访问,否则后续 add_node 会连不上。配置监听地址的常规位置是 /var/lib/pgsql/18/data/postgresql.conf:

listen_addresses = '*'

改完记得重启 PostgreSQL 服务。

3.3 初始化集群和加载扩展

PostgreSQL 18 启动后,连接上去创建 Citus 扩展。这一步有两个关键点:一是共享预加载库要配置好,二是扩展要在同一个会话里先创建,不能半路断掉。

编辑 postgresql.conf,添加或确认以下配置:

shared_preload_libraries = 'citus'

然后重启数据库,再执行:

CREATE EXTENSION citus;

如果之前没有在 postgresql.conf 里设置 shared_preload_libraries,直接执行 CREATE EXTENSION 会报错,提示需要重启。这一步的顺序千万别搞反,我见过有人配了扩展但忘记改预加载库,结果类型和函数一半能用一半不能用,排查了半天。

启动集群后,先把自己这台机器指定为协调器,再把工作节点加进来:

SELECT citus_set_coordinator_host('192.168.10.10', 5432); SELECT citus_add_node('192.168.10.11', 5432); SELECT citus_add_node('192.168.10.12', 5432);

验证节点是否都上线了:

SELECT * FROM citus_get_active_worker_nodes();

正确的输出应该包含刚才加入的两个工作节点的地址和端口。如果你看到节点数为 0,先去检查节点之间的网络和 pg_hba.conf,这是新手最常踩的坑。

4. 核心实操:分布表、引用表和查询设计

集群搭起来了,接下来这步决定了你是“用上了分布式”还是“用好了分布式”。我见过太多团队装了 Citus,结果核心表全是广播查询,性能反而比单机更差,最后得出“分布式不靠谱”的结论——其实问题出在建模阶段。

4.1 分布列怎么选:三个硬性原则

分布列的选择直接决定分片的数据分布和路由效率。按照我的经验,至少要满足三个条件。

第一个是业务查询里的高频过滤条件。大多数订单系统的查询都会带 user_id 或 tenant_id,这是天然分布列。比如“查这个人最近半年的订单”,如果按 order_id 哈希分布,那一条查询可能打到所有分片;按 user_id 分布,直接路由到单个分片,性能差一个数量级。

第二个是分布列的取值要足够离散、足够均匀。像性别这种只有两个值的字段,分片再多也没用,所有行基本都挤在少数分片上,数据倾斜严重。选分布列之前,最好用“select 字段,count(*) group by 字段 order by 2 desc”扫一遍,看看头部取值占比。如果某个取值占比超过两成,这个字段就不适合做分布列。

第三个是尽量让高频 JOIN 的表共享同一个分布列。Citus 对于相同分布列的表 JOIN 可以本地执行;分布列不同就只能在协调器层面做重分布,数据来回传,性能下降非常明显。实战里最常见的组合是:orders 按 user_id 分布,user_events 也按 user_id 分布,这样两张表 JOIN 时每个分片各自完成,协调器只做结果汇总。

4.2 创建分布表和引用表:具体 SQL 操作

假设我们有个电商库,核心表是订单表和用户行为表,维度表是商品表,标准的建表分布操作如下。

先建订单表,注意主键必须包含分布列:

CREATE TABLE orders ( order_id bigint, user_id int NOT NULL, sku_id int, amount numeric(12, 2), create_time timestamp, PRIMARY KEY (order_id, user_id) );

这里我把主键设计成 (order_id, user_id) 而不是单独的 order_id,就是因为 Citus 的硬性要求:分布式表的主键必须包含分布列。如果你实在想保留单一主键,那只能在 postgresql.conf 里关掉外键验证,但从数据一致性考虑我不建议这么做。

然后按 user_id 分布订单表:

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

像 sku_id 对应的商品表,数据量不大但 JOIN 频次高,适合做成引用表:

CREATE TABLE products ( sku_id int PRIMARY KEY, name text, category text ); SELECT create_reference_table('products');

创建完之后,可以用下面几条 SQL 验证分片情况和数据分布:

SELECT * FROM citus_shards; SELECT citus_relation_size('orders'); SELECT shardid, nodename, shard_size FROM citus_shards WHERE relation_name = 'orders';

假如发现分片在节点上放得不均匀,可以用自带的 rebalance 操作重新打散:

SELECT citus_rebalance_start();

这个函数会把分片从负载高的节点迁移到负载低的节点,执行过程在线,业务无感知。日常运维中,新增工作节点后第一件事就是跑这个。

4.3 查询的几种模式和性能预期

物理模型定好了,再说说查询怎么写才能吃到分布式红利。

理想查询是带分布列等值条件的单分片查询。比如:

SELECT sum(amount), date_trunc('month', create_time) FROM orders WHERE user_id = 1001 AND create_time > now() - interval '6 months' GROUP BY 2;

这条 SQL 会直接路由到 user_id=1001 所在的分片,Citus 内部的执行计划里只有一条 Remote SQL,没有广播、没有合并,性能和单机查一张小表的体验差不多。

次优情况是带分布列但结果是聚合型查询,协调器会把 GROUP BY 下推到每个分片,然后再汇总一遍。比如:

SELECT date_trunc('month', create_time), count(*) FROM orders WHERE create_time > now() - interval '1 year' GROUP BY 1;

这种 SQL 每个分片算各自几个月的订单数,协调器拿到局部结果再按月份合并,数据量小很多,实际也很快。

最不推荐的情况是全表扫描型查询,比如一个不带头部大客户的全局报表,每条 SQL 都要扫描所有分片,协调器要等最慢分片返回才算结束。这种工作负载不是 Citus 的强项,如果业务里大量存在这种查询,说明建模阶段分布列选错了,或者说这个业务根本不适合分片。

4.4 扩容和重平衡:从 3 节点扩到 10 节点的路径

Citus 加节点很简单,核心是把新节点的标识写进集群元数据,然后让空闲分片迁移过去。

新节点装好 PG 和 Citus 之后,在协调器上执行:

SELECT citus_add_node('192.168.10.13', 5432);

然后触发重平衡:

SELECT citus_rebalance_start();

这里有个运维细节,生产环境建议把默认的最大传输速率调低,免得迁移分片的 IO 把业务查询拖垮。相关参数是 citus.max_worker_processes 和 citus.max_parallel_workers_per_connection,但迁移带宽跟 PostgreSQL 主从复制的 wal_keep_size、max_wal_senders 也有关系。如果分片表很大,理论上迁移一个分片就是一次 PostgreSQL 级联复制,耗时会比较久,建议业务低峰期操作。

5. 常见问题与排查实录

这节把所有踩过的坑集中整理一遍,很多问题不是看官方文档就能快速定位的。

5.1 故障速查表

现象可能原因排查方向
CREATE EXTENSION citus 失败shared_preload_libraries 未设置或未重启检查 postgresql.conf,重启 PG 后重试
节点状态是 inactive节点间网络不通或 pg_hba.conf 未放行ping 测试,检查监听地址和认证配置
查询特别慢,查看执行计划发现全库广播查询条件没带分布列重新审视分布列选择,或改写 SQL
报错主键冲突,但业务上明明是唯一键主键未包含分布列调整主键约束,将分布列加入主键
UPDATE 跨分片报错事务目标行分布在不同分片重试或重新设计分布列,尽量让事务局部化
新增节点后分片迁移不动元数据没刷新或 rebalance 没触发先查询 citus_get_active_worker_nodes,确认节点在线
协调器节点内存持续上涨大量广播查询,协调器成为瓶颈优化查询,增加工作节点,协调器升级配置

这个表里的每一条我都实际碰到过,尤其是第一条和第三条,占了新手问题的八成以上。

5.2 数据倾斜排查:citus_check_health 立大功

数据倾斜在分布式系统里是个隐蔽问题,表面看节点都在忙,实际上只有一台忙到冒烟。Citus 提供了内置健康检查函数:

SELECT * FROM citus_check_health();

这个函数会返回集群内各维度的检查结果,包括分片分布、引用表同步、元数据一致性等。我最常看的是分片分布情况:

SELECT nodename, count(*) shard_cnt, sum(shard_size) total_size FROM citus_shards WHERE relation_name = 'orders' GROUP BY 1 ORDER BY 3 DESC;

如果某个节点的 total_size 明显比其他节点大,说明分布列选得不够均匀,后续查询会在这个热点分片上积压。排除方法是用上面说的占比分析看头部取值,业务上如果确实存在超大租户,一种变通办法是给热点 key 单独拆表、单独做二次分布,但这属于高级用法,一般业务用不上。

5.3 运维监控建议:别让协调器成为单点

很多团队把 Citus 部署完了就扔在生产环境,监控只看主节点。这里必须提醒一句:协调器本身就是 PostgreSQL 实例,它挂了整个集群入口就没了。生产环境一定要给协调器做主从复制,搭配 Patroni 或者 repmgr 这类高可用工具,切换方案和普通 PostgreSQL 主从一致。

工作节点的监控重点则不同,除了常规 CPU、内存、磁盘,还要盯分片分布是否均匀、跨节点复制延迟是否增大。Citus 官方提供了半官方的 Prometheus exporter,社区里也有现成看板,我拿到集群第一件事就是把分片分布和节点健康状态做成截图,每天早上一睁眼先扫一眼。另外记录一下:citus_worker_stat_activity 视图可以查看工作节点上正在执行的后台查询,排查慢查询时很有用,比挨个节点 psql 上去看效率高得多。

写在最后的实操体会

折腾 Citus 这几年,最大的体会是:分布式不是银弹,它解决的是“水平扩容”这个问题,但代价是建模思维要变。你不能继续用单库单表的思路设计表结构,从第一张表开始,就要想清楚未来的并发模型、查询模型和数据分布模型。比如我踩过的“分布式事务变慢”的坑,当时如果舍得把订单表按 tenant_id 而不是 create_time 来做分布,很多问题根本不会出现。PostgreSQL 18 配合最新版 Citus,性能表现和数据一致性比早期版本强了不止一个档次,新项目如果预判三年后会到百亿级行数,一开始就把分布列定对,后面会省下无数改表的时间和加班费。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询