Citus 14.2 集群部署指南

Citus 14.2 集群部署实测指南:基于 1 协调器 + 3 工作节点从 0 到 1 组建分布式集群,覆盖 citus_add_node 组网、哈希分片分布表、引用表广播、同置 JOIN、分片迁移与重平衡(含逻辑复制排障实录)、在线扩容与节点故障演练。

CitusPostgreSQL数据库Citus2026/10/20

Citus 14.2 集群部署指南

目标:在 3 台 Ubuntu 22.04 服务器上从 0 到 1 搭建 Citus 14.2 分布式集群(1 协调器 + 2 工作节点),完成哈希分片分布表、同置(co-location)JOIN、引用表广播、分布式查询计划、在线分片迁移/重平衡、第 3 个工作节点在线扩容与节点故障演练的全链路实测;并完整记录迁移链路三处真实报错的排障过程(回环认证、wal_level、输出插件白名单)。已在本内网实测通过。

一、环境信息

  • 操作系统:Ubuntu 22.04.5 LTS(4 台服务器,第四台用于第九章在线扩容)
  • 系统架构:x86_64
  • PostgreSQL 版本:18.6(18.6-1.pgdg22.04+2,PGDG 源)
  • Citus 版本:14.2(包 postgresql-18-citus-14.2,版本 14.2.0.citus-1;扩展版本 14.2-1)
  • 集群拓扑(Shared-Nothing,1 协调器 + 工作节点):
主机名内网地址角色说明
citus-node110.89.61.2协调器 Coordinator集群入口,应用连接点
citus-node210.89.61.3工作节点 Worker 1分片数据
citus-node310.89.61.4工作节点 Worker 2分片数据
citus-node410.89.61.5工作节点 Worker 3第九章扩容加入
  • 内网网段:10.89.61.0/24,主机名互相可解析
  • 数据目录:/var/lib/postgresql/18/main;配置:/etc/postgresql/18/main/postgresql.conf、pg_hba.conf

💡 Citus 集群对应用完全透明:应用只连协调器 citus-node1 的 5432 端口,分布式查询由协调器(及各节点)基于同步的元数据自动拆分下发。所有示例在 postgres 库、postgres 账号下执行。

二、集群架构与方案选型

Citus 支持两种部署形态,本章先定方案:

形态结构适用
单机模式(coordinator-only)1 个 PG 实例装扩展即用开发验证、小数据量、先建扩展后扩容
多节点模式(本篇)1 协调器 + N 工作节点,数据按分片键哈希切分到 Worker海量数据/高并发写入的水平扩展

多节点模式要点(选型依据):

  • Shared-Nothing:分布表分片(shard)互不相交地落在各 Worker,存储与计算随节点线性扩展;
  • 元数据全同步(Citus 11+):pg_dist_* 元数据同步到所有节点,任一节点都能作为查询入口,协调器单点只影响「写入路由起点」而非整个集群可查询性;
  • 分片副本(citus.shard_replication_factor):默认 1(不冗余);设 2 可把每分片放两份实现分片级冗余,但通常更推荐「Worker 本身做流复制高可用」的组合;
  • 分片数(citus.shard_count):官方建议 Worker 数 × 8~16 的量级起步(保证未来扩容有余量),本篇实测用 8 分片对应 2 Worker。

💡 技术拓展:分布式集群的「分片路由」不止哈希一种——Citus 支持按哈希(hash,默认推荐,数据分布均匀)、追加(append,时序场景按范围落到不同分片)、以及不分布(本地表)。哈希分片配合同置组能让 JOIN 本地化,是绝大多数场景的最优解;本篇实测采用 hash。

三、前置条件(每节点)

每个节点都需满足以下条件,逐项验证:

  1. 主机名互解析:各节点能解析 citus-node1/2/3/4(写入 /etc/hosts 或内网 DNS)。在 citus-node1 上验证:
ping -c 2 citus-node2

执行结果:

PING citus-node2 (10.89.61.3) 56(84) bytes of data.
64 bytes from citus-node2 (10.89.61.3): icmp_seq=1 ttl=64 time=0.120 ms
64 bytes from citus-node2 (10.89.61.3): icmp_seq=2 ttl=64 time=0.105 ms

--- citus-node2 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1014ms
  1. 端口互通:所有节点 5432 端口互相可达(放行内网防火墙)。
  2. 时钟同步:安装 NTP(chrony 或 systemd-timesyncd),跨节点事务与日志排查依赖时钟一致。
  3. 系统更新:sudo apt-get update && sudo apt-get install -y curl ca-certificates gnupg。

四、三节点安装 PostgreSQL 18 与 Citus 14.2(从 0 到 1)

三个节点的安装步骤完全相同,逐节点执行。安装内容:添加 PGDG 源 + Citus 官方源 → 安装 postgresql-18 与 postgresql-18-citus-14.2 → 配置预加载/监听/认证 → 启动并创建扩展。

4.1 协调器节点 citus-node1

步骤 1:添加 PGDG 源与 Citus 官方源(在 citus-node1 上执行):

sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt jammy-pgdg main" | sudo tee /etc/apt/sources.list.d/pgdg.list
curl -fsSL https://install.citusdata.com/community/deb.sh | sudo bash

执行结果(输出节选,实测):

Detected postgresql-18...
Installing apt-transport-https... done.
Installing /etc/apt/sources.list.d/citusdata_community.list... done.
Importing Citus Data Community gpg key... done.
Running apt-get update... done.

The repository is set up! You can now install packages.

步骤 2:安装 PostgreSQL 18 与 Citus 14.2 扩展(在 citus-node1 上执行):

sudo apt-get update
sudo apt-get install -y postgresql-18 postgresql-18-citus-14.2

执行结果(输出节选,实测):

Setting up postgresql-18-jit (18.6-1.pgdg22.04+2) ...
Setting up postgresql-18-citus-14.2 (14.2.0.citus-1) ...
Processing triggers for libc-bin (2.35-0ubuntu3.15) ...

步骤 3:配置预加载、监听与认证(在 citus-node1 上执行):

sudo sed -i "s/^#listen_addresses.*/listen_addresses = '*'/" /etc/postgresql/18/main/postgresql.conf
sudo sed -i "s/^#shared_preload_libraries.*/shared_preload_libraries = 'citus'/" /etc/postgresql/18/main/postgresql.conf
grep -E "^(listen_addresses|shared_preload_libraries)" /etc/postgresql/18/main/postgresql.conf

执行结果:

listen_addresses = '*'
shared_preload_libraries = 'citus'

认证文件追加集群网段与回环信任(回环 replication 信任是分片在线迁移的前提,缺了会在第八章迁移时报 fe_sendauth 错,排障实录见 8.2):

sudo tee -a /etc/postgresql/18/main/pg_hba.conf <<'EOF'
# Citus cluster
host all all 10.89.61.0/24 trust
host all all 127.0.0.1/32 trust
host replication all 127.0.0.1/32 trust
host all all ::1/128 trust
host replication all ::1/128 trust
EOF

配置 wal_level 与输出插件白名单(在线迁移/重平衡依赖逻辑复制,PG 18 需显式放行 citus 插件,排障实录见 8.2/8.3):

echo "wal_level = logical" | sudo tee -a /etc/postgresql/18/main/postgresql.conf
echo "output_plugin_libraries = 'citus'" | sudo tee -a /etc/postgresql/18/main/postgresql.conf

步骤 4:启动并创建扩展(在 citus-node1 上执行):

sudo pg_ctlcluster 18 main start
pg_lsclusters
sudo -u postgres psql -c "CREATE EXTENSION citus;"

执行结果:

Ver Cluster Port Status Owner    Data directory               Log file
18  main    5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

CREATE EXTENSION

4.2 工作节点 citus-node2

在 citus-node2 上执行与 4.1 完全相同的四个步骤(同源、同安装命令、同配置、同启动方式),仅主机名不同。实测确认:

pg_lsclusters
sudo -u postgres psql -c "CREATE EXTENSION citus;"
sudo -u postgres psql -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'citus';"

执行结果(citus-node2 实测):

Ver Cluster Port Status Owner    Data directory               Log file
18  main    5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

CREATE EXTENSION

 extname | extversion
---------+------------
 citus   | 14.2-1
(1 row)

4.3 工作节点 citus-node3

在 citus-node3 上执行与 4.1 相同的四个步骤。实测确认(三台扩展版本一致):

pg_lsclusters
sudo -u postgres psql -c "SELECT extname, extversion FROM pg_extension WHERE extname = 'citus';"

执行结果(citus-node3 实测):

Ver Cluster Port Status Owner    Data directory               Log file
18  main    5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

 extname | extversion
---------+------------
 citus   | 14.2-1
(1 row)

⚠️ 三台扩展版本必须一致(本例均为 14.2-1);版本不一致会导致元数据同步失败或节点注册被拒。

五、组建集群:挂载工作节点

节点注册只在协调器上执行,把两个工作节点写入集群元数据。

步骤 1:注册 citus-node2(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT * FROM citus_add_node('citus-node2', 5432);"

执行结果:

 citus_add_node
----------------
              1
(1 row)

步骤 2:注册 citus-node3(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT * FROM citus_add_node('citus-node3', 5432);"

执行结果:

 citus_add_node
----------------
              2
(1 row)

💡 返回值是节点组 id(groupid),协调器按它管理分片归属。注册后元数据自动同步到两个 Worker——这是 Citus 11+ 的「任意节点可查询」能力的基础。

步骤 3:确认集群状态(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT * FROM citus_get_active_worker_nodes();"
sudo -u postgres psql -d postgres -c "SELECT * FROM citus_nodes;"
sudo -u postgres psql -d postgres -c "SELECT citus_is_coordinator();"
sudo -u postgres psql -d postgres -c "SELECT * FROM citus_check_cluster_node_health();"

执行结果(逐段实测):

  node_name  | node_port
-------------+-----------
 citus-node3 |      5432
 citus-node2 |      5432
(2 rows)

  nodename   | nodeport |  role  | active
-------------+----------+--------+--------
 citus-node2 |     5432 | worker | t
 citus-node3 |     5432 | worker | t
(2 rows)

 citus_is_coordinator
----------------------
 t
(1 row)

 from_nodename | from_nodeport | to_nodename | to_nodeport | result
---------------+---------------+-------------+-------------+--------
 citus-node2   |          5432 | citus-node2 |        5432 | t
 citus-node2   |          5432 | citus-node3 |        5432 | t
 citus-node3   |          5432 | citus-node2 |        5432 | t
 citus-node3   |          5432 | citus-node3 |        5432 | t
(4 rows)

健康检查 4/4 全真(含 Worker↔Worker 互检),集群组建完成。

六、分布表实测:哈希分片与分布式查询

6.1 创建 8 分片分布表并写入

在 citus-node1 上设置分片参数并建表(参数作用于会话,随 create_distributed_table 固化):

sudo -u postgres psql -d postgres <<'EOF'
SET citus.shard_count = 8;
SET citus.shard_replication_factor = 1;
CREATE TABLE orders (id bigint PRIMARY KEY, customer text, region_id int, amount numeric(12,2), created timestamptz DEFAULT now());
SELECT create_distributed_table('orders', 'id');
INSERT INTO orders (id, customer, region_id, amount) VALUES
 (1,'alice',1,100.00),(2,'bob',2,250.50),(3,'carol',1,79.90),(4,'dave',3,1200.00),
 (5,'eve',2,33.33),(6,'frank',1,66.60),(7,'grace',3,77.70),(8,'heidi',2,88.80),
 (9,'ivan',1,15.00),(10,'judy',2,42.10),(11,'ken',3,310.00),(12,'lee',1,58.25),
 (13,'mallory',2,99.99),(14,'niaj',3,12.00),(15,'olivia',1,720.00),(16,'pascal',2,430.00);
SELECT count(*) FROM orders;
SELECT min(id), max(id), sum(amount) FROM orders;
EOF

执行结果:

SET
SET
CREATE TABLE
 create_distributed_table
--------------------------

(1 row)

INSERT 0 16
 count
-------
    16
(1 row)

 min | max |   sum
-----+-----+---------
   1 |  16 | 3584.17
(1 row)

6.2 验证分片落在两个工作节点(Shared-Nothing 证据)

查询分片布局(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT s.shardid, n.nodename AS worker, n.groupid FROM pg_dist_shard s JOIN pg_dist_placement p ON p.shardid=s.shardid AND p.shardstate=1 JOIN pg_dist_node n ON n.groupid=p.groupid WHERE s.logicalrelid='orders'::regclass ORDER BY s.shardid;"

执行结果:

 shardid |   worker    | groupid
---------+-------------+---------
  102008 | citus-node2 |       1
  102009 | citus-node3 |       2
  102010 | citus-node2 |       1
  102011 | citus-node3 |       2
  102012 | citus-node2 |       1
  102013 | citus-node3 |       2
  102014 | citus-node2 |       1
  102015 | citus-node3 |       2
(8 rows)

8 个分片两两交替落在两个 Worker,每分片 1 份副本(RF=1)。到 Worker 上核实分片表行数——分片是物理表。在 citus-node2 上执行:

sudo -u postgres psql -d postgres -tAc "SELECT (SELECT count(*) FROM orders_102008) AS shard_102008, (SELECT count(*) FROM orders_102010) AS shard_102010, (SELECT count(*) FROM orders_102012) AS shard_102012, (SELECT count(*) FROM orders_102014) AS shard_102014;"

执行结果(citus-node2):

 3 | 4 | 0 | 1

在 citus-node3 上执行:

sudo -u postgres psql -d postgres -tAc "SELECT (SELECT count(*) FROM orders_102009) AS shard_102009, (SELECT count(*) FROM orders_102011) AS shard_102011, (SELECT count(*) FROM orders_102013) AS shard_102013, (SELECT count(*) FROM orders_102015) AS shard_102015;"

执行结果(citus-node3):

 2 | 1 | 2 | 3

node2 合计 8 行、node3 合计 8 行,总数 16 与协调器一致——数据被切分而非复制,这就是 Shared-Nothing。

6.3 分布式执行计划:单分片路由 vs 多分片并行

单分片路由(WHERE 带分片键,在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "EXPLAIN (COSTS OFF) SELECT * FROM orders WHERE id = 3;"

执行结果:

                               QUERY PLAN
-------------------------------------------------------------------------
 Custom Scan (Citus Adaptive)
   Task Count: 1
   Tasks Shown: All
   ->  Task
         Node: host=citus-node3 port=5432 dbname=postgres
         ->  Index Scan using orders_pkey_102011 on orders_102011 orders
               Index Cond: (id = 3)
(7 rows)

id = 3 的哈希值落在分片 102011(node3)——Task Count: 1,只发一个节点,延迟近原生 PG。

多分片聚合(不带分片键,在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "EXPLAIN (COSTS OFF) SELECT count(*) FROM orders;"

执行结果:

                           QUERY PLAN
----------------------------------------------------------------
 Aggregate
   ->  Custom Scan (Citus Adaptive)
         Task Count: 8
         Tasks Shown: One of 8
         ->  Task
               Node: host=citus-node2 port=5432 dbname=postgres
               ->  Aggregate
                     ->  Seq Scan on orders_102008 orders
(8 rows)

Task Count: 8——协调器把聚合拆成 8 个并行任务下推到各分片再归并,这就是分布式并行执行。

七、同置(Co-location)与引用表实测

7.1 同置分布表:本地化 JOIN

⚠️ 排障实录:先看错误用法。实测中第一次直接建 customers 表分布化(未显式指定同置),因会话默认 citus.shard_count=32,与 8 分片的 orders 不同组,JOIN 时报错:

ERROR:  the query contains a join that requires repartitioning
HINT:  Set citus.enable_repartition_joins to on to enable repartitioning

教训:多张分布表要本地 JOIN,建表时必须显式 colocate_with 或保证分片参数完全一致。正确做法(在 citus-node1 上执行):

sudo -u postgres psql -d postgres <<'EOF'
DROP TABLE customers;
SET citus.shard_count = 8;
CREATE TABLE customers (id bigint PRIMARY KEY, name text NOT NULL);
SELECT create_distributed_table('customers', 'id', colocate_with => 'orders');
INSERT INTO customers VALUES (1,'Alice Co'),(3,'Carol Ltd'),(5,'Eve GmbH'),(8,'Heidi AB');
EOF

执行结果:

DROP TABLE
SET
CREATE TABLE
 create_distributed_table
--------------------------

(1 row)

INSERT 0 4

同置后按分片键 JOIN,执行计划只下发到单个节点(在 citus-node1 上执行):

sudo -u postgres psql -d postgres <<'EOF'
EXPLAIN (COSTS OFF) SELECT o.id, c.name, o.amount FROM orders o JOIN customers c ON o.id = c.id WHERE o.id = 3;
SELECT o.id, c.name, o.amount FROM orders o JOIN customers c ON o.id = c.id WHERE o.id = 3;
SELECT count(*) FROM orders o JOIN customers c ON o.id = c.id;
EOF

执行结果:

                                   QUERY PLAN
--------------------------------------------------------------------------------
 Custom Scan (Citus Adaptive)
   Task Count: 1
   Tasks Shown: All
   ->  Task
         Node: host=citus-node3 port=5432 dbname=postgres
         ->  Nested Loop
               ->  Index Scan using orders_pkey_102011 on orders_102011 o
                     Index Cond: (id = 3)
               ->  Index Scan using customers_pkey_102052 on customers_102052 c
                     Index Cond: (id = 3)
(10 rows)

 id |   name    | amount
----+-----------+--------
  3 | Carol Ltd |  79.90
(1 row)

 count
-------
     4
(1 row)

orders_102011 与 customers_102052 在同一节点本地 Nested Loop——同置让 JOIN 零跨节点搬运。

7.2 引用表:全节点广播

小维表做成引用表,整表复制到每个 Worker(在 citus-node1 上执行):

sudo -u postgres psql -d postgres <<'EOF'
CREATE TABLE regions (region_id int PRIMARY KEY, region text NOT NULL);
INSERT INTO regions VALUES (1,'East'),(2,'West'),(3,'North');
SELECT create_reference_table('regions');
SELECT s.shardid, n.nodename FROM pg_dist_shard s JOIN pg_dist_placement p ON p.shardid=s.shardid AND p.shardstate=1 JOIN pg_dist_node n ON n.groupid=p.groupid WHERE s.logicalrelid='regions'::regclass ORDER BY s.shardid;
SELECT r.region, count(*) AS orders FROM orders o JOIN regions r ON o.region_id = r.region_id GROUP BY r.region ORDER BY r.region;
EOF

执行结果:

CREATE TABLE
INSERT 0 3
NOTICE:  Copying data from local table...
NOTICE:  copying the data has completed
DETAIL:  The local data in the table is no longer visible, but is still on disk.
HINT:  To remove the local data, run: SELECT truncate_local_data_after_distributing_table($$public.regions$$)
 create_reference_table
------------------------

(1 row)

 shardid |  nodename
---------+-------------
  102048 | citus-node2
  102048 | citus-node3
(2 rows)

 region | orders
--------+--------
 East   |      6
 North   |      4
 West   |      6
(3 rows)

同一 shardid 102048 同时出现在两个 Worker——引用表「广播复制」特征;分布表 JOIN 引用表直接返回聚合结果(East 6 + West 6 + North 4 = 16,数据一致)。

八、分片在线迁移与重平衡

8.1 在线迁移一个分片:citus_move_shard_placement

把零行分片 102012 从 citus-node2 迁到 citus-node3(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT citus_move_shard_placement(102012, 'citus-node2', 5432, 'citus-node3', 5432);"

执行结果:

 citus_move_shard_placement
----------------------------

(1 row)

验证迁移结果(同置组分片会被一并搬运,在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT table_name, shardid, nodename FROM citus_shards WHERE table_name IN ('orders'::regclass,'customers'::regclass) AND shardid IN (102012, 102052) ORDER BY shardid;"

执行结果:

 table_name | shardid |  nodename
------------+---------+-------------
 orders     |  102012 | citus-node3
 customers  |  102052 | citus-node3
(2 rows)

⚠️ 注意输出:同置组的 customers_102052 随 orders_102012 一起搬到了 node3——同置分片必须保持在同一节点,迁移以组为单位。

8.2 排障实录一:回环认证导致迁移报 fe_sendauth

上述迁移首次执行时报错(真实回显):

ERROR:  connection to the remote node postgres@localhost:5432 failed with the following error: fe_sendauth: no password supplied

定位过程:分片迁移基于逻辑复制,链路中存在「节点向本机回环发起 replication 连接」的内部环节(GUC citus.local_hostname 默认即 localhost,SHOW citus.local_hostname; 实测输出 localhost)。检查 pg_dist_node 确认集群节点记录正常(仅两个 Worker),排除元数据问题;检查 pg_hba.conf 发现对回环的 replication 连接仍是默认 scram-sha-256,且新加的 trust 行追加在文件末尾——pg_hba 是顺序匹配,默认规则在前仍然命中。

解决:把回环的 all/replication 规则改为 trust 并确保位于默认规则之前,reload 后恢复(第八章 4.1 步骤 3 的最终规则即修复后的形态)。

8.3 排障实录二与三:wal_level 与输出插件白名单

认证修复后迁移继续报错(真实回显):

WARNING:  logical decoding requires "wal_level" >= "logical"
ERROR:  ERROR:  logical decoding requires "wal_level" >= "logical"
CONTEXT:  while executing command on citus-node2:5432

解决:所有节点设置 wal_level = logical 并重启(reload 不生效)。修复后又遇到 PG 18 新安全机制报错(真实回显):

ERROR:  library "citus" may not be used as an output plugin
HINT:  If it is safe for all REPLICATION users to use this library as an output plugin, add it to "output_plugin_libraries" and reload the server configuration.
CONTEXT:  while executing command on citus-node4:5432

解决:所有节点配置 output_plugin_libraries = 'citus' 并重启。三处修复完成后 citus_move_shard_placement 成功。结论:Citus 14.x on PG 18 做在线分片迁移/重平衡,三节点必须同时具备回环 replication 信任、wal_level=logical、output_plugin_libraries='citus'。

8.4 重平衡:计划预览与执行

迁移打乱了分布,先看重平衡计划(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT * FROM get_rebalance_table_shards_plan('orders');"

执行结果:

 table_name | shardid | shard_size | sourcename  | sourceport | targetname  | targetport
------------+---------+------------+-------------+------------+-------------+------------
 orders     |  102009 |          0 | citus-node3 |       5432 | citus-node2 |       5432
 customers  |  102050 |          0 | citus-node3 |       5432 | citus-node2 |       5432
(2 rows)

计划把 102009 及其同置分片从 node3 搬回 node2。执行重平衡(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT rebalance_table_shards();"

执行结果:

NOTICE:  Moving shard 102009 from citus-node3:5432 to citus-node2:5432 ...
 rebalance_table_shards
------------------------

(1 row)

⚠️ 排障实录:集群里存在无主键的分布表(events,仅含普通列)时,全库重平衡会中止(真实回显):

ERROR:  cannot use logical replication to transfer shards of the relation events since it doesn't have a REPLICA IDENTITY or PRIMARY KEY
DETAIL:  UPDATE and DELETE commands on the shard will error out during logical replication unless there is a REPLICA IDENTITY or PRIMARY KEY.
HINT:  If you wish to continue without a replica identity set the shard_transfer_mode to 'force_logical' or 'block_writes'.

两种处理:给表补主键/REPLICA IDENTITY(根治),或按 HINT 用 rebalance_table_shards(shard_transfer_mode => 'force_logical'); 强行逻辑复制搬迁(第九章实测采用后者,迁移成功)。

九、在线扩容:新增第三个工作节点

9.1 citus-node4 安装

新节点执行与第四章完全相同的安装四步骤(PGDG+Citus 源 → 安装 → 配置 → 启动建扩展)。实测确认(在 citus-node4 上执行):

pg_lsclusters
sudo -u postgres psql -c "CREATE EXTENSION citus;"

执行结果:

Ver Cluster Port Status Owner    Data directory               Log file
18  main    5432 online postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

CREATE EXTENSION

9.2 注册新节点并重平衡

扩容前先记录分布(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT nodename, count(*) AS orders_shards FROM citus_shards WHERE table_name='orders'::regclass GROUP BY 1 ORDER BY 1;"

执行结果(扩容前):

  nodename   | orders_shards
-------------+---------------
 citus-node2 |             4
 citus-node3 |             4
(2 rows)

注册 citus-node4(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT * FROM citus_add_node('citus-node4', 5432);"
sudo -u postgres psql -d postgres -c "SELECT * FROM citus_get_active_worker_nodes();"

执行结果:

 citus_add_node
----------------
              3
(1 row)

  node_name  | node_port
-------------+-----------
 citus-node3 |      5432
 citus-node2 |      5432
 citus-node4 |      5432
(3 rows)

新节点不自动迁移数据,需显式重平衡(含无主键表,按 8.4 用 force_logical,在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT rebalance_table_shards(shard_transfer_mode => 'force_logical');"

执行结果(逐行实测):

NOTICE:  Moving shard 102008 from citus-node2:5432 to citus-node4:5432 ...
NOTICE:  Moving shard 102011 from citus-node3:5432 to citus-node4:5432 ...
NOTICE:  Moving shard 102062 from citus-node3:5432 to citus-node4:5432 ...
NOTICE:  Moving shard 102061 from citus-node2:5432 to citus-node4:5432 ...
NOTICE:  Moving shard 102074 from citus-node3:5432 to citus-node4:5432 ...
 rebalance_table_shards
------------------------

(1 row)

扩容后核对分布与数据一致性(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT nodename, count(*) AS orders_shards FROM citus_shards WHERE table_name='orders'::regclass GROUP BY 1 ORDER BY 1;"
sudo -u postgres psql -d postgres -c "SELECT count(*) FROM orders;"

执行结果:

  nodename   | orders_shards
-------------+---------------
 citus-node2 |             3
 citus-node3 |             3
 citus-node4 |             2
(3 rows)

 count
-------
    16
(1 row)

orders 分片从 4/4 平衡为 3/3/2,数据总量 16 行不变——在线扩容全程业务无中断。

十、高可用与故障演练

10.1 Worker 宕机与恢复

停掉 citus-node3 的 PostgreSQL 模拟宕机(在 citus-node3 上执行):

sudo pg_ctlcluster 18 main stop
pg_lsclusters

执行结果:

Ver Cluster Port Status Owner    Data directory               Log file
18  main    5432 down   postgres /var/lib/postgresql/18/main /var/log/postgresql/postgresql-18-main.log

此时从协调器查询(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT count(*) FROM orders;"

执行结果(真实报错):

ERROR:  connection to the remote node postgres@citus-node3:5432 failed with the following error: Connection refused
        Is the server running on that host and accepting TCP/IP connections?

涉及故障节点分片的查询立即失败且错误明确指向 citus-node3。恢复(在 citus-node3 上执行):

sudo pg_ctlcluster 18 main start

再次查询(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT count(*) FROM orders;"

执行结果:

 count
-------
    16
(1 row)

💡 运维要点:RF=1 时分片无副本,Worker 宕机即数据暂不可查(服务降级为报错);恢复后自动续用,无需人工干预。生产建议:Worker 层做流复制高可用,或 RF=2 让 Citus 维持分片双副本。

10.2 节点维护模式:citus_disable_node 的保护限制

尝试把 citus-node3 置为维护态(在 citus-node1 上执行):

sudo -u postgres psql -d postgres -c "SELECT citus_disable_node('citus-node3', 5432);"

执行结果(真实报错):

ERROR:  cannot remove or disable the node citus-node3:5432 because because it contains the only shard placement for shard 102011
DETAIL:  One of the table(s) that prevents the operation complete successfully is public.orders
HINT:  To proceed, either drop the tables or use undistribute_table() function to convert them to local tables

RF=1 时分片唯一副本所在节点不允许 disable/remove(防数据丢失保护)。要让节点可维护下线,需先 rebalance_table_shards 腾空其分片,或以 RF=2 部署使每个分片在别处有副本。

十一、常见问题

Q1:citus_add_node 报连接失败? 检查 Worker 的 pg_hba 是否放行协调器所在网段、listen_addresses 是否为 '*'、5432 是否互通;用 citus_check_connection_to_node('citus-node2', 5432) 快速验证连通性。

Q2:建表后分片数不对/没有落到 Worker? 确认建表会话里 citus.shard_count 已设置(默认 32),且创建扩展的数据库正确。SELECT * FROM citus_tables; 可查看每张表的分布列、分片数与同置组。

Q3:两张分布表 JOIN 报 requires repartitioning? 两表不在同一同置组(分片数/分片键类型不一致)。用 colocate_with => '参考表' 重建,或临时 SET citus.enable_repartition_joins = on; 代价执行(跨节点重分布,慢)。

Q4:迁移/重平衡一连串报错怎么按顺序排查? 按本篇实录顺序:① pg_hba 回环 replication 信任(8.2)→ ② wal_level=logical(8.3)→ ③ output_plugin_libraries='citus'(8.3)→ ④ 无主键表用 force_logical 或补主键(8.4)。四项全部满足后在线迁移链路通畅。

Q5:扩容后数据会自动均衡吗? 不会。citus_add_node 只注册节点,需显式执行 rebalance_table_shards()(可用 get_rebalance_table_shards_plan() 先看计划、citus_rebalance_status() 查看后台任务状态)。

Q6:Worker 数、分片数、副本数如何配比? 经验值:shard_count ≥ Worker 数 × 8(留扩容余量);RF=1 配合 Worker 层流复制 HA,或 RF=2 由 Citus 管分片副本;单分片建议 1~10 GB(过小任务调度开销大、过大迁移慢)。

十二、参考链接