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”。

逻辑复制槽会持续保留 WAL,如果订阅端长时间断开,发布端磁盘可能被 WAL 撑满。生产环境务必监控 pg_replication_slots 的 restart_lsn 与当前 LSN 的差距,并设置合理的 wal_sender_timeout 与告警。
逻辑复制环境:发布端 pub(5432) → 订阅端 sub(5433) 发布端实例 pub · port 5432 postgresql.conf wal_level = logical max_replication_slots = 10 max_wal_senders = 10 (三项均需重启实例生效) pg_hba.conf host replication repl_user 127.0.0.1/32 host all repl_user 127.0.0.1/32 method: scram-sha-256 发布端对象 表 orders PUBLICATION pub_orders 复制槽 pub_orders(逻辑解码,钉住 WAL) walsender 进程 → 发送解码后的变更 订阅端实例 sub · port 5433 postgresql.conf wal_level = replica(无需 logical) max_replication_slots = 10 max_wal_senders = 10 (订阅端不负责解码) 订阅端对象 表 orders SUBSCRIPTION sub_orders apply 进程:重放 INSERT/UPDATE/DELETE 初始同步:COPY 快照(表已有数据时) pg_stat_subscription 查看状态 验证:INSERT id=1001 → 订阅端可查 失败先查槽 active / apply pid / 主键 复制连接 replication 逻辑变更流 INSERT/UPDATE/DELETE 关键约束:发布端 wal_level=logical;pg_hba 放行 replication;两端表主键一致;复制槽持续保留 WAL