4. 搭建首个逻辑复制环境
逻辑复制与物理流复制最大的区别在于:物理复制复制的是 WAL 字节流,要求主备实例的物理结构完全一致;而逻辑复制复制的是“行级变更的逻辑含义”,订阅端把发布端解码出来的 INSERT/UPDATE/DELETE 重新执行一遍。因此逻辑复制不要求两端数据目录一致,甚至允许两端表结构存在合理差异(例如订阅端多一个索引、多一列带默认值的字段)。但代价是:发布端必须把 WAL 级别调到 logical,必须为逻辑解码预留复制槽,必须允许复制连接登录,并且发布端与订阅端的表必须有可匹配的主键或唯一约束。
本节的目标是:在一台机器上启动两个 PostgreSQL 实例(端口 5432 作为发布端 pub,端口 5433 作为订阅端 sub),完成参数与认证配置,创建 publication 与 subscription,并验证一条 INSERT 能跨实例同步过去。下面按“发布端配置 → 订阅端配置 → 创建发布订阅 → 验证与排障”的顺序展开。
一、发布端 postgresql.conf 的三个关键参数
逻辑复制对发布端有三个硬性参数要求,缺一不可:
- wal_level = logical:这是逻辑复制的前提。默认值 replica 只记录足够物理复制和崩溃恢复的信息,不包含逻辑解码所需的元数据(如旧元组标识、表 OID 映射等)。改为 logical 后,WAL 中会额外记录这些信息,因此 WAL 体积会增大,写入开销略有上升。该参数需要重启实例生效。
- max_replication_slots:逻辑复制槽的数量上限。每个订阅在发布端占用一个槽,槽会“钉住”WAL,防止订阅端还没消费的 WAL 被回收。默认值通常为 10,单订阅场景够用,但生产环境要按“订阅数 + 预留”规划。该参数需要重启生效。
- max_wal_senders:允许同时运行的 WAL 发送进程数。逻辑复制在发布端也通过 walsender 进程把解码结果发给订阅端,因此这个值必须大于等于“物理备库数 + 逻辑订阅数”。默认值 10,一般够用,但若同时有多个物理备库和逻辑订阅,需要上调。该参数同样需要重启生效。
在发布端实例的 postgresql.conf 中修改如下(示例值,按实际规模调整):
# 发布端 pub (port 5432) postgresql.conf
listen_addresses = 'localhost'
port = 5432
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
# 逻辑解码需要保留足够 WAL,避免槽被快速推进时找不到起点
wal_sender_timeout = 60s
注意:wal_level 从 replica 改为 logical 后,必须重启实例,而不是 reload。可以用下面的语句确认当前生效值:
SHOW wal_level;
SHOW max_replication_slots;
SHOW max_wal_senders;
如果 wal_level 仍显示 replica,说明实例没有重启,逻辑复制槽创建会直接报错。
二、订阅端 postgresql.conf 的配置
订阅端不负责解码,因此 wal_level 保持默认的 replica 即可,不需要改成 logical。订阅端真正需要关注的是:它要作为客户端去连接发布端,因此需要能发起复制连接;同时订阅端的 apply 进程会把变更写入本地表,这部分走普通 SQL 路径,不依赖 wal_level。订阅端示例配置:
# 订阅端 sub (port 5433) postgresql.conf
listen_addresses = 'localhost'
port = 5433
wal_level = replica
max_replication_slots = 10
max_wal_senders = 10
订阅端保留 max_replication_slots 和 max_wal_senders 是为了将来它自己也可能成为上游,属于预留配置,不是逻辑复制订阅的必要条件。
三、pg_hba.conf:允许复制连接
逻辑复制在建立连接时,订阅端会以“复制模式”连接发布端,因此发布端的 pg_hba.conf 必须有一条 database 为 replication 的记录。如果只写了普通 database 记录,订阅端会报“no pg_hba.conf entry for replication connection”。发布端 pg_hba.conf 示例:
# 发布端 pub pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
host replication repl_user 127.0.0.1/32 scram-sha-256
host all repl_user 127.0.0.1/32 scram-sha-256
这里创建了一个专用复制用户 repl_user,并授予 REPLICATION 权限。逻辑复制槽的创建还要求用户对目标表有 SELECT 权限,或者使用超级用户。示例中为了简化,给 repl_user 授予了 REPLICATION 和 LOGIN,并对业务表授予 SELECT:
-- 在发布端执行
CREATE ROLE repl_user LOGIN REPLICATION PASSWORD 'Repl@2024';
CREATE DATABASE appdb;
\c appdb
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer text NOT NULL,
amount numeric(10,2) NOT NULL,
status text NOT NULL DEFAULT 'created',
created_at timestamptz NOT NULL DEFAULT now()
);
GRANT SELECT ON orders TO repl_user;
订阅端也需要一个能写入本地表的用户,通常直接用本地超级用户或业务用户即可。订阅端建同名表(结构可以略有差异,但主键必须一致):
-- 在订阅端执行
CREATE DATABASE appdb;
\c appdb
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer text NOT NULL,
amount numeric(10,2) NOT NULL,
status text NOT NULL DEFAULT 'created',
created_at timestamptz NOT NULL DEFAULT now()
);
四、创建发布与订阅
发布端创建 publication,指定要发布的表。这里用 FOR TABLE 精确发布,避免误发布整库:
-- 发布端 appdb
CREATE PUBLICATION pub_orders FOR TABLE orders;
订阅端创建 subscription,连接串指向发布端,并指定 publication 名称。注意连接串中的 host/port/dbname/user 必须与发布端 pg_hba.conf 匹配:
-- 订阅端 appdb
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=127.0.0.1 port=5432 dbname=appdb user=repl_user password=Repl@2024'
PUBLICATION pub_orders;
执行 CREATE SUBSCRIPTION 时,订阅端会立即连接发布端、创建复制槽、并启动初始数据同步(如果表已有数据,会先做 COPY 快照)。如果这一步卡住,通常是网络、认证或复制槽权限问题。
五、验证同步与常见排障
验证分三步:先看发布端复制槽是否创建成功,再看订阅端状态,最后做一次真实写入。
-- 发布端:查看复制槽
SELECT slot_name, plugin, slot_type, active
FROM pg_replication_slots;
-- 订阅端:查看订阅状态
SELECT subname, pid, relid, received_lsn, latest_end_lsn
FROM pg_stat_subscription;
-- 发布端:写入一条数据
INSERT INTO orders (id, customer, amount, status)
VALUES (1001, 'alice', 199.00, 'created');
-- 订阅端:确认数据已同步
SELECT * FROM orders WHERE id = 1001;
如果订阅端查不到数据,按以下顺序排查:第一,发布端 pg_replication_slots 中槽是否存在且 active 为 t;第二,订阅端 pg_stat_subscription 是否有 pid,若 pid 为空说明 apply 进程没起来;第三,检查发布端日志是否有“logical decoding found consistent point”之类的启动信息;第四,确认表有主键或 REPLICA IDENTITY 设置,否则 UPDATE/DELETE 无法定位行,会报“cannot update table because it does not have a replica identity”。