Skip to content

PG16:逻辑复制增强 ​

PostgreSQL 16 是逻辑复制自 PG10 落地以来改版最密集的一个大版本,官方 release notes 中逻辑复制相关条目多达十余条。三个方向尤其突出:备库逻辑解码(逻辑复制不再强依赖主库,从物理备库也能发布数据)、并行应用大事务(订阅端 streaming = parallel,大事务的回放从串行变并行)与应用权限模型的收紧(apply 按表属主执行,新增 run_as_owner 选项与 pg_create_subscription 预定义角色)。此外还有二进制格式的初始表同步、ORIGIN 选项防复制回环、无主键表的回放性能优化等一批实打实的工程改进。以上能力均新增于 PG16,本页逐条展开、给出配置与验证方法,全部条目以官方 release notes 为准。

在展开之前,先校准一个容易被误传的参数传闻:任务中常见的「PG16 把 max_replication_slots / max_wal_senders 默认值从 10 提高」的说法未见于 PG16 release notes——PG16 中这两个参数的默认值仍为 10,没有变化。需要扩容时请按业务负载显式设置,而不是依赖「升级自动变大」。

逻辑复制的原理、发布与订阅的基础机制在前面章节已经完整覆盖(原理见逻辑复制原理),本页聚焦「PG16 相对 PG15 的增量」:每个特性先讲它解决了 PG15 时代的什么问题,再讲怎么用、代价与验证手段。

前置知识

建议先阅读:PG16:聚合与窗口函数增强

基础概念 ​

PG16 逻辑复制改进总览 ​

特性解决的 PG15 痛点关键配置 / 对象
备库逻辑解码逻辑发布强绑定主库备库 + pg_log_standby_snapshot()
并行应用大事务大事务订阅端串行回放,延迟堆积streaming = parallel
初始表同步二进制拷贝全量同步走文本格式,大表慢且丢精度订阅 binary 选项
apply 按表属主执行订阅 owner 权限过大run_as_owner 选项
pg_create_subscription 预定义角色创建订阅需要超级用户角色授权
password_required = false特权连接必须带密码订阅选项
REPLICA IDENTITY FULL 走索引无主键表回放全表扫描自动(无需配置)
ORIGIN 选项级联/环形拓扑回环控制粗糙CREATE SUBSCRIPTION ... ORIGIN
wal_retrieve_retry_interval 按订阅生效全局参数互相拖累订阅选项
订阅统计提前创建pg_stat_subscription_stats 缺初始条目自动
walsender 进程标题含数据库名多库环境排障无法定位自动

备库逻辑解码改变了什么拓扑假设 ​

PG15 及以前,逻辑解码只能在主库进行——发布端必须是接受写入的实例。这带来两个结构性限制:一是发布流量与业务写入共享主库 CPU;二是主库切换(物理高可用)后,逻辑复制槽随之丢失,订阅全部断流。PG16 引入备库逻辑解码后,逻辑解码可以在热备库上运行,典型新拓扑:

text
       物理流复制                逻辑复制
主库 ────────────────► 备库 ────────────────► 下游订阅端
(业务写入)         (逻辑发布/解码)        (汇聚/分析)

发布、订阅与回放的完整模型回顾见逻辑复制原理,订阅端选项的完整清单见订阅(CREATE SUBSCRIPTION)。

并行应用的工作进程结构 ​

PG15 的 streaming = on 只解决了「发布端内存」问题(大事务边产生边流式发送,订阅端落临时文件),但应用阶段仍是串行:事务提交前攒下的变更要等提交后一次性顺序回放。PG16 的 streaming = parallel 在订阅端为大事务引入并行 apply worker:

进程职责
leader apply worker接收变更流,把大事务的子事务分派给并行 worker
parallel apply worker并行回放大事务的各个子事务(受 GUC 限制数量)

监控侧:pg_stat_subscription 新增 leader_pid 列标记「该 worker 是并行 worker,leader 是谁」;新增等待事件 LogicalParallelApplyMain、LogicalParallelApplyStateChange、LogicalApplySendData。

发布端与订阅端的参数总表 ​

参数端默认值说明
wal_level发布端replica必须 logical,重启生效
max_replication_slots发布端10物理槽 + 逻辑槽总数(PG16 默认未变)
max_wal_senders发布端10walsender 上限(PG16 默认未变)
max_worker_processes订阅端8后台 worker 总池
max_logical_replication_workers订阅端4apply / 同步 / 并行 worker 的逻辑复制配额
max_sync_workers_per_subscription订阅端2初始同步并行度
max_parallel_apply_workers_per_subscription订阅端2新增于 PG16:单订阅并行 apply 上限
max_active_replication_origins订阅端10复制原点上限
track_commit_timestamp双端off开启后可按提交时间溯源与做原点级冲突检测

所有订阅侧 worker 都从 max_worker_processes 总池分配,三层预算的正确关系是「总池 ≥ 逻辑复制配额 ≥ 各订阅 worker 之和」。扩容口诀:先扩总池,再分逻辑配额,最后按订阅分并行度——顺序颠倒会出现「配额够但总池空」的静默等待,日志表现为 worker 申请失败后反复重试。

语法与用法 ​

备库逻辑解码的启用与验证 ​

conf
# 备库 postgresql.conf:逻辑解码的前提与主库一致
wal_level = logical
max_replication_slots = 10

备库上创建逻辑槽时,PG16 提供了一个配套函数解决「备库无法自发产生快照 WAL 记录」的初始化问题:

sql
-- 在备库执行:写入一条 standby 快照 WAL 记录,允许逻辑槽创建
SELECT pg_log_standby_snapshot();

-- 随后正常创建逻辑槽
SELECT * FROM pg_create_logical_replication_slot('pub_slot', 'pgoutput');

验证解码在备库上工作:

bash
# 在备库消费解码流
pg_recvlogical -h standby-host -d app --slot pub_slot --start -f -

主库上做一次 INSERT,备库的解码流应输出对应的逻辑变更。备库的只读属性对发布端 walsender 无影响——解码是「读 WAL」,不是「写数据」。

并行应用:创建订阅与参数 ​

sql
CREATE SUBSCRIPTION sub_orders
    CONNECTION 'host=publisher port=5432 dbname=app user=repl password=secret'
    PUBLICATION pub_orders
    WITH (
        copy_data = true,
        streaming = parallel           -- 新增于 PG16 的取值
    );
conf
# 订阅端 postgresql.conf
max_parallel_apply_workers_per_subscription = 4   -- 新增于 PG16
max_logical_replication_workers = 8
max_worker_processes = 16                          -- 所有 worker 的总量池

观察并行 apply:

sql
-- worker 类型与 leader 归属(leader_pid 新增于 PG16)
SELECT subname, worker_type, pid, leader_pid, received_lsn, latest_end_lsn
FROM pg_stat_subscription;

-- 等待事件:并行 worker 常态应处于 LogicalParallelApplyMain / StateChange
SELECT pid, wait_event_type, wait_event
FROM pg_stat_activity
WHERE backend_type = 'walsender' OR application_name LIKE 'sub%';

二进制格式的初始同步 ​

订阅的 binary 选项(PG14 引入)请求发布端以二进制格式传输增量数据;PG16 把二进制格式扩展到了初始表同步阶段:

sql
CREATE SUBSCRIPTION sub_orders
    CONNECTION 'host=publisher port=5432 dbname=app user=repl password=secret'
    PUBLICATION pub_orders
    WITH (copy_data = true, binary = true);

全量拷贝走二进制意味着时间戳、numeric、大整数不再经过文本序列化/反序列化,初始同步更快且规避文本格式在极端值上的转换歧义。前提是两端的数据类型在二进制格式上兼容(跨大版本订阅时由驱动协商,官方条目明确「只有标记为 binary 的订阅才能使用」)。

run_as_owner 与权限模型 ​

PG16 起,订阅端 apply worker 执行 SELECT/DML 默认以各表的属主身份进行(官方条目:Perform logical replication SELECT and DML actions as the table owner),这大幅缩小了订阅 owner 的实际权限面。旧行为(一切以订阅 owner 身份执行)通过选项保留:

sql
CREATE SUBSCRIPTION sub_orders
    CONNECTION '...'
    PUBLICATION pub_orders
    WITH (run_as_owner = true);     -- 恢复 PG15 行为

配套的约束:订阅 owner 必须是超级用户,或对所有相关表属主具备 SET ROLE 权限(否则创建订阅报错)。

pg_create_subscription 预定义角色与免密码连接 ​

sql
-- DBA 准备:授予创建订阅的专用角色(新增于 PG16)
GRANT pg_create_subscription TO etl_admin;

-- etl_admin 现在可以创建订阅(仍需数据库上的 CREATE 权限)
CREATE SUBSCRIPTION sub_facts
    CONNECTION '...'
    PUBLICATION pub_facts;

-- 机器账户 + 证书/peer 认证场景:允许订阅连接不带密码(新增于 PG16)
CREATE SUBSCRIPTION sub_internal
    CONNECTION 'host=publisher dbname=app user=repl'
    PUBLICATION pub_internal
    WITH (password_required = false);

password_required = false 只在订阅 owner 是超级用户(或显式授权)时可用,用于证书认证与本地 socket 的服务间连接,不要在普通密码认证场景使用。

ORIGIN 选项与回环防护 ​

sql
-- 只应用「无来源标记」的本地变更:级联/环形拓扑防回环(新增于 PG16)
CREATE SUBSCRIPTION sub_cascade
    CONNECTION '...'
    PUBLICATION pub_cascade
    WITH (origin = none);

origin = none 表示订阅端只应用发布端「本地产生」的变更(没有复制原点标记的),从上游同步下来的带标记变更不再向下转发。这与复制原点(replication origin)机制配合,替代了 PG15 时代只能靠拓扑设计规避回环的做法。

详细说明 ​

备库逻辑解码的实现边界 ​

备库逻辑解码依赖一个主库上不存在的东西:快照 WAL 记录。主库在创建逻辑槽时会记录一条快照,用于确定「哪些事务已完成、可以解码」;备库的 WAL 全部来自主库重放,自发产生不了这类记录。PG16 的解法有两层:

  • 备库上的逻辑槽创建会等待/请求快照记录,pg_log_standby_snapshot() 显式触发一条;
  • 备库创建的逻辑槽只存在于备库(槽不随物理复制传播),主备切换后该槽不会出现在新主库上——它锚定的是备库自己的解码状态。

由此推论几个运维约束:

  • 备库逻辑槽的 WAL 保留压力作用在主库 → 备库的流复制链路上(备库不能回收主库发来的 WAL),监控 pg_replication_slots 的位置要在备库执行;
  • 备库逻辑解码的输出不含主备切换后「主库本地」的变更——切换时间线分叉后,旧备库槽继续解码的是新主库重放下来的流(若备库继续作为备库);
  • pg_log_standby_snapshot() 也会被自动周期触发(活跃系统上无需手工频繁调用)。

并行 apply 的调度与限制 ​

streaming = parallel 的并行范围是单个大事务内部的子事务,而不是「多个事务并行」——事务顺序由 leader apply worker 严格按提交顺序调度,避免破坏可见性语义。三个工程推论:

  • 收益集中在大事务(大量子事务、或单事务内变更行数多)的场景;小事务流(典型 OLTP 变更)几乎无并行机会;
  • 并行 worker 数量受 max_parallel_apply_workers_per_subscription 限制,且所有 worker 都从 max_worker_processes 池子分配——调高它时确认总量池够用;
  • 并行应用失败的事务,其 finish LSN 可能未记录,ALTER SUBSCRIPTION ... SKIP 排障时先切回 streaming = on 复现定位,再决定跳过策略。

订阅端监控三件套:pg_stat_subscription 的 worker_type / leader_pid、pg_stat_subscription_stats 的错误计数、pg_stat_activity 的等待事件。与流复制延迟监控的关系见流复制监控。

二进制初始同步的兼容性前提 ​

二进制格式把「文本协商」换成「按 OID 的二进制表示」,两端类型的二进制格式必须一致。跨大版本订阅(发布端 PG16 → 订阅端 PG15)时,新版本类型若改了二进制表示会导致数据错误——官方在 PG16 的条目里明确该能力「只对标记为 binary 的订阅生效」,实践中:

  • 同版本族(16↔16)的 binary = true 初始同步是安全的默认优化;
  • 跨版本迁移场景保持 binary = false(默认),让文本格式做兜底转换;
  • 自定义类型(扩展提供的)两端必须都安装且二进制格式一致。

初始同步的两条路线 ​

copy_data = true 的内置初始同步(table synchronization worker 逐表 COPY)适合「可以直接停写或可容忍追赶」的场景。大表迁移更常用的是 pg_dump 预加载 + 增量订阅:

bash
# 1) 发布端导出结构
pg_dump -h publisher --schema-only -d app > schema.sql
# 2) 订阅端导入结构
psql -h subscriber -d app -f schema.sql
# 3) 数据全量(低峰执行)
pg_dump -h publisher -d app --data-only -t orders | psql -h subscriber -d app
sql
-- 4) 建订阅:跳过初始拷贝,只接增量
CREATE SUBSCRIPTION sub_orders
    CONNECTION 'host=publisher dbname=app user=repl password=secret'
    PUBLICATION pub_orders
    WITH (copy_data = false);

copy_data = false 意味着「假设表内容已就绪」,只消费订阅建立之后的变更——预加载与建订阅之间若发布端仍有写入,这部分变更会漏掉。严格一致的做法是:先在发布端手工创建逻辑槽固定起点(CREATE SUBSCRIPTION ... WITH (create_slot = false, slot_name = 'my_slot') 复用该槽),让全量导出与槽的快照锚定在同一位置。路线选择的经验值:单表 10GB 以内走内置同步省心;大表 / 多表长窗口走预加载,把「追赶增量」的时间从天级压到小时级。

增量应用的顺序与可见性保证 ​

订阅端回放遵循两条不变式,理解它们才能正确设计下游:

  1. 事务边界保持:发布端的一个事务在订阅端也是一个事务,要么全部应用、要么全部不应用;并行 apply 的并行范围只发生在「事务内部」的子事务之间,跨事务的提交顺序不变;
  2. 按提交顺序应用:leader apply worker 按发布端的提交顺序分发,下游看到的行状态序列与发布端一致。

由此的推论:

  • 订阅端延迟曲线上的「尖峰」来自大事务(提交后才整体可见),streaming = parallel 缩短的是大事务的回放时长,不改变可见性时点;
  • 长事务提交前的变更不发布(逻辑解码在提交时才输出完整事务),发布端的超长事务会推迟其后所有增量;
  • 应用侧不应假设「事务内的行级变更按行序到达下游」,只应依赖事务边界——这与单机复制语义一致;
  • 流式发送中(streaming 开启)的大事务在提交前对订阅端查询不可见,不要期望「实时读到大事务的中间状态」。

run_as_owner 前后的权限对照 ​

执行者PG15PG16 默认PG16 run_as_owner = true
初始同步 COPY订阅 owner表属主订阅 owner
增量 SELECT/DML订阅 owner表属主订阅 owner

权限收紧带来的迁移检查点:

  • 表属主必须在订阅端拥有目标表的一切操作权限(天经地义,但要确认属主关系没被 ALTER TABLE OWNER 弄乱);
  • 订阅 owner 若非超级用户,需要对所有表属主有 SET ROLE 权限:GRANT role_of_table_owner TO subscription_owner;
  • 依赖「apply 以 owner 身份执行」来隐式获得权限的老部署,升级 PG16 后反而更宽松(owner 天然有权限);依赖「订阅 owner 万能」的安全审计则要重新评估。

REPLICA IDENTITY FULL 的索引优化(新增于 PG16) ​

无主键表常配 REPLICA IDENTITY FULL 让 UPDATE/DELETE 可复制,代价是订阅端回放时用「旧值全匹配」定位行——PG15 及以前是顺序扫描,大表上每行变更都是一次全表扫。PG16 起回放会利用表上的 B-tree 索引(按旧值等值查找)代替顺序扫描:

sql
-- 发布端:无主键表 + FULL
ALTER TABLE events_nopk REPLICA IDENTITY FULL;
-- 订阅端:给「旧值里区分度高、且能建唯一/近唯一索引」的列建索引
CREATE INDEX ON events_nopk (event_uid, occurred_at);

适用前提:表上存在覆盖「定位所需列」的 B-tree 索引;没有索引的表行为不变(仍顺序扫)。这条优化与复制标识的选择策略一起,详见逻辑复制限制。

wal_retrieve_retry_interval 按订阅生效(新增于 PG16) ​

PG15 及以前它是全局参数:一个订阅的下游断连/重试节奏会影响所有订阅。PG16 起改为订阅级选项,创建订阅时指定:

sql
CREATE SUBSCRIPTION sub_slow
    CONNECTION '...'
    PUBLICATION pub_slow
    WITH (wal_retrieve_retry_interval = '5s');

多订阅共享一个订阅端实例时,把「对延迟不敏感的汇总订阅」与「实时订阅」分开设置重试间隔,互不拖累。配套新增了 LogicalRepLauncherDSA、LogicalRepLauncherHash 两个等待事件。

订阅统计与进程标题的观测改进(新增于 PG16) ​

两处小而实用的监控改进:

  • 订阅统计条目在创建时即生成:pg_stat_subscription_stats 不再等到第一条统计上报才出现,stats_reset 语义因此准确——监控系统能在订阅刚建立时就配置好指标;
  • 逻辑 walsender 进程标题包含数据库名:ps 输出从 walsender repl 127.0.0.1 变为包含具体数据库,多库多订阅的主库上定位「哪个订阅连接在吃 CPU」不再需要查 pg_stat_replication 反推。

发布过滤与订阅的组合 ​

发布端的过滤维度决定「送什么」,订阅端的选择决定「收什么」。PG16 沿用 PG15 的完整过滤能力:

层级发布端写法版本
表级CREATE PUBLICATION p FOR TABLE a, b;PG10+
行级FOR TABLE t WHERE (region = 'cn');PG15+
列级FOR TABLE t (id, name);PG15+
schema 级FOR TABLES IN SCHEMA s;PG15+
全部表FOR ALL TABLES;PG10+

与订阅端选项的配合要点:

  • 行/列过滤作用于发布端解码,网络上只传过滤后的数据——带宽节省发生在上游;
  • 列过滤下,订阅端表「未发布的列」必须可空或有默认值,否则初始同步 COPY 失败;
  • 行过滤的 WHERE 引用列应属于复制标识(replica identity),否则发布 UPDATE/DELETE 时定位与过滤会失配——建发布时服务端会直接报错拦住;
  • FOR ALL TABLES 的发布会把未来新建的表自动纳入,多租户场景省心,但新表的 REPLICA IDENTITY 检查要靠流程保证。

订阅端本地写入的冲突类型速查 ​

订阅端「不是白纸」时(本地有写入或历史数据),apply 可能遇到四类冲突:

冲突触发条件典型处置
唯一键冲突两端各自插入同一主键确认权威方,清理订阅端数据或改用不同的键生成策略
外键冲突引用行尚未就位(同步顺序问题)初始同步先于增量;检查是否漏了某张表的同步
结构失配两端列数/类型不一致两端结构对齐后重建订阅(DDL 不复制)
对象缺失表/约束在订阅端不存在建表后再建订阅;检查 schema 与 search_path

冲突时 apply worker 会停在该事务上反复重试,错误留在日志与 pg_stat_subscription_stats;定位「卡在哪个事务」看日志中的 LSN 与 pg_stat_subscription.latest_end_lsn 的停滞点。多对一拓扑下冲突概率天然更高——汇聚前统一主键策略是设计期动作,不是运维期补救。

监控 SQL 清单 ​

sql
-- 1) 订阅端:各 worker 的进度(worker_type / leader_pid 新增于 PG16)
SELECT subname, worker_type, pid, leader_pid,
       received_lsn, latest_end_lsn
FROM pg_stat_subscription;

-- 2) 发布端:订阅连接的发送延迟
SELECT application_name, state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), sent_lsn)  AS pending_bytes,
       pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS confirmed_lag_bytes
FROM pg_stat_replication
WHERE application_name LIKE 'sub%';

-- 3) 复制槽保留的 WAL(在拥有槽的一端查询)
SELECT slot_name, active,
       pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) AS retained_bytes
FROM pg_replication_slots
WHERE slot_type = 'logical';

-- 4) 错误计数(PG16 起创建订阅即有条目)
SELECT subid, subname, apply_error_count, sync_error_count
FROM pg_stat_subscription_stats;

判读要点:retained_bytes 持续增长而 active = f,是订阅端掉线的第一信号;apply_error_count 增长说明存在数据冲突;pending_bytes 大而 confirmed_lag_bytes 稳定,说明发布端在发、订阅端在追,瓶颈在网络或 apply 而不是解码。

备库发布的监控视角差异 ​

发布端在备库时,监控的「放置位置」与主库发布不同:

  • pg_stat_replication:查订阅连接所在的那一端——备库发布时,订阅连接出现在备库的 pg_stat_replication,主库看不到;
  • pg_replication_slots:备库的逻辑槽只显示在备库;主库看到的是「流复制槽」,两者是不同的槽;
  • WAL 保留压力的传导:备库逻辑槽不消费 → 备库无法回收从主库收到的 WAL → 主库视角里「备库 replay 延迟」增大。链式传导让「订阅端离线」最终表现为「物理备库延迟」,告警要跨两端关联。

巡检脚本给这两类槽分别留查询,并在监控面板标注「备库 + 逻辑槽」的拓扑——这是备库发布拓扑下最容易遗漏的运维细节。

订阅端在这一切里是「旁观者」:logical replication launcher 常驻订阅端,发布端在主备之间漂移时订阅端只感知「连接断开重连」;因此发布端拓扑变化的告警要挂在发布端自身,而不是订阅端。

常见故障与排查路径 ​

症状首查处置
apply 报错、延迟持续增长pg_stat_subscription_stats 的 apply_error_count修复数据冲突;确认后 ALTER SUBSCRIPTION ... SKIP 跳过该事务
worker 数不足、并行起不来max_worker_processes 总池与日志的申请失败信息扩总池或降并行配额
初始同步极慢表大小、binary 是否开启大表改走 pg_dump 预加载
发布端 WAL 堆积pg_replication_slots 的 retained_bytes恢复订阅连接;max_slot_wal_keep_size 兜底
run_as_owner 报权限错误订阅 owner 对表属主的 SET ROLE 链补授权,或评估 run_as_owner = true
备库发布的槽在主库看不到槽位于备库到备库端查询与维护
sql
-- 事务级跳过(订阅端):定位到报错事务的 finish LSN 后
ALTER SUBSCRIPTION sub_orders SKIP (lsn = '0/2A0B1C00');

SKIP 只应作为「已确认该事务可丢弃」的最后手段——跳过的变更不会重放。先在 streaming = on 模式下复现拿到完整错误(并行模式下的 finish LSN 可能缺失,见注意事项),再决定跳过还是修复数据。

实战示例 ​

示例一:备库发布 + 双端验证 ​

bash
# 主库:确保 wal_level = logical(重启生效)
# 备库:流复制已就绪,创建复制用户与逻辑槽
psql -h standby-host -U postgres -d app <<'SQL'
SELECT pg_log_standby_snapshot();
SELECT * FROM pg_create_logical_replication_slot('pub_slot', 'pgoutput');
SQL
bash
# 备库:消费解码流(观察窗口)
pg_recvlogical -h standby-host -d app --slot pub_slot --start -f - &
bash
# 主库:写入变更
psql -h primary-host -U postgres -d app \
  -c "INSERT INTO public.orders (id, amount) VALUES (9001, 199) ON CONFLICT (id) DO NOTHING;"
text
pg_recvlogical 输出:
table public.orders: INSERT: id[bigint]:9001 amount[numeric]:199

清理演示槽:SELECT pg_drop_replication_slot('pub_slot');(在备库执行)。

示例二:并行 apply 的大事务订阅 ​

bash
# 发布端:建发布(原理见 publication 页)
psql -h primary-host -U postgres -d app -c \
  "CREATE PUBLICATION pub_events FOR TABLE public.events;"
sql
-- 订阅端
CREATE SUBSCRIPTION sub_events
    CONNECTION 'host=primary-host dbname=app user=repl password=secret'
    PUBLICATION pub_events
    WITH (copy_data = true, streaming = parallel);

-- 发布端制造一个大批量事务
INSERT INTO public.events (uid, kind, payload)
SELECT gen_random_uuid()::text, 'batch', '{}'
FROM generate_series(1, 2000000);
sql
-- 订阅端观察并行 worker 的启动与状态流转
SELECT subname, worker_type, pid, leader_pid, received_lsn, latest_end_lsn
FROM pg_stat_subscription ORDER BY subname;
text
  subname   | worker_type |  pid  | leader_pid | received_lsn | latest_end_lsn
------------+-------------+-------+------------+--------------+----------------
 sub_events | apply       | 31001 |            | 0/2A0B1C00   | 0/2A0B1C00
 sub_events | parallel    | 31002 |      31001 |              |
 sub_events | parallel    | 31003 |      31001 |              |

parallel worker 挂在 leader 31001 名下处理大事务子段;事务回放完成后 worker 退出,pg_stat_subscription 恢复单 apply 行。对比 streaming = on 的同场景(单 worker 串行回放),订阅端延迟曲线的尖峰明显收窄。

示例三:级联拓扑 + origin 防回环 ​

text
主库 A ──逻辑复制──► 汇聚库 B ──逻辑复制──► 归档库 C
sql
-- B 端:订阅 A(只收 A 的本地变更)
CREATE SUBSCRIPTION sub_b_from_a
    CONNECTION 'host=a-host dbname=app user=repl password=secret'
    PUBLICATION pub_a
    WITH (origin = none);

-- C 端:订阅 B
CREATE SUBSCRIPTION sub_c_from_b
    CONNECTION 'host=b-host dbname=app user=repl password=secret'
    PUBLICATION pub_b
    WITH (origin = none);

origin = none 保证 C 收到的永远是「源头在 A 的原始变更」,B 上由订阅产生的变更(带复制原点标记)不会被再次转发;任何一端误建反向订阅也不会形成 A→B→C→A 的回环放大。冲突类型与回环场景的深入讨论见逻辑复制限制。

示例四:pg_dump 预加载 + 免停机增量接管 ​

bash
# 发布端:建发布
psql -h publisher -d app -c "CREATE PUBLICATION pub_orders FOR TABLE public.orders;"

# 1) 结构先行
pg_dump -h publisher -d app --schema-only | psql -h subscriber -d app

# 2) 数据全量(低峰执行;大表可按表拆分)
pg_dump -h publisher -d app --data-only --table=public.orders | psql -h subscriber -d app
sql
-- 3) 订阅端:跳过初始拷贝,只接增量
CREATE SUBSCRIPTION sub_orders
    CONNECTION 'host=publisher dbname=app user=repl password=secret'
    PUBLICATION pub_orders
    WITH (copy_data = false);
sql
-- 4) 观察追赶进度(发布端)
SELECT application_name, state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS lag_bytes
FROM pg_stat_replication
WHERE application_name = 'sub_orders';
-- lag_bytes 趋近 0 后,下游业务即可切入

注意第 2、3 步之间发布端的写入会漏掉——演示环境停写即可,生产切换的严格做法(快照锚定)见「初始同步的两条路线」。另外 SEQUENCE 不在逻辑复制范围内(PG16 依然如此),接管前要把订阅端序列拨到发布端当前值。

示例五:多对一汇聚拓扑 ​

把两个区域的订单汇聚到中央分析库(多对一是逻辑复制的经典形态):

sql
-- 中央库(订阅端):两个订阅分别指向两个区域
CREATE SUBSCRIPTION sub_region_a
    CONNECTION 'host=region-a dbname=app user=repl password=secret'
    PUBLICATION pub_orders WITH (origin = none, streaming = on);

CREATE SUBSCRIPTION sub_region_b
    CONNECTION 'host=region-b dbname=app user=repl password=secret'
    PUBLICATION pub_orders WITH (origin = none, streaming = on);
sql
-- 汇聚端观察:两个订阅各自独立计量
SELECT subname, worker_type, pid,
       received_lsn, latest_end_lsn
FROM pg_stat_subscription
ORDER BY subname;

工程要点:

  • 两个发布端的表主键不能冲突(UUID 或带区域前缀),否则订阅端唯一约束会撕裂两条链路;
  • origin = none 挡住「汇聚库再转发」的回环;若中央库还向下游发布,下游订阅同样声明;
  • 两个订阅的 worker 都占 max_worker_processes 池,按订阅数预留;中央库的重放写入还要与本地分析负载抢资源,容量规划按「重放 I/O + 分析 I/O」合计;
  • 汇聚端错误排查用 pg_stat_subscription_stats 按订阅分别看 apply_error_count,冲突往往只发生在其中一条链路。

注意事项 ​

备库逻辑槽不会随主备切换迁移。 备库上建的槽锚定备库自身的解码状态,切换后新主库没有它;依赖备库发布的拓扑要为切换准备「重建槽 + 重建订阅」的预案(PG17 的 failover 槽机制才解决跨切换保留)。

并行 apply 排障要先降级。 streaming = parallel 下事务失败时 finish LSN 可能缺失,ALTER SUBSCRIPTION ... SKIP 无法精确定位;先把订阅改成 streaming = on 复现并确定 LSN,再执行跳过。

worker 总量池要统筹。 并行 apply worker、初始同步 worker、apply worker 全部从 max_worker_processes 分配;调高 max_parallel_apply_workers_per_subscription 前先算总账,否则 worker 拉不起来(日志可见等待)。

binary 初始同步对跨版本与自定义类型敏感。 混合版本或带扩展类型的链路保持默认文本格式;确需 binary 时做一次全类型清单的兼容核对(尤其 numeric、时间、扩展类型)。

run_as_owner 迁移前梳理属主链。 升级 PG16 后 apply 默认以表属主执行,订阅 owner 非超级用户时必须补 SET ROLE 授权链,否则创建/刷新订阅直接报错——把它写进升级清单而不是撞到再说。

备库发布的 WAL 保留压力在主库链路。 备库逻辑槽长期不消费会拖住主备之间的 WAL 回收,max_slot_wal_keep_size 兜底与 pg_replication_slots 监控照常要做,只是监控视角换到备库。

password_required = false 的边界。 仅限超级用户/特权 owner 场景;普通业务订阅保留密码校验,连接串密码纳入密钥管理。

无主键优化不是万能。 PG16 的 REPLICA IDENTITY FULL 索引查找要求「订阅端表上有合适的 B-tree 索引」;两端索引不对齐时发布端以为在优化、订阅端仍在顺序扫,两端索引策略要一起维护。

序列与 DDL 永远不在复制范围内。 切换流量前把订阅端 SEQUENCE setval 到发布端当前值并做对照验证;发布/订阅两端的结构变更(加列、改类型)要人工双向同步,先改订阅端、再改发布端。

apply 报错先看错误计数再动数据。 pg_stat_subscription_stats 的计数器区分 apply / sync 两类错误;带着「先跳过再说」的心态处理冲突,会让数据分叉变成不可审计的黑箱。

SKIP 与冲突修复都要留痕。 每次跳过事务记下 LSN、错误与决策依据,进入变更记录;无痕跳过攒多了,两端数据的一致性就再也说不清了。

最佳实践 ​

  1. 发布端尽量选备库,主库只留业务写入。 PG16 的备库逻辑解码让「发布不抢主库 CPU」从设计愿望变成默认拓扑;主备切换的订阅重建预案要预先演练。
  2. 大事务订阅显式启用 streaming = parallel。 小事务场景保持 off/on 即可;并行度从 max_parallel_apply_workers_per_subscription = 2 起步压测,不要一步到位拉高。
  3. max_worker_processes 总账先行。 物理 worker 池 = 物理 worker 常规占用 + 逻辑复制 worker + 同步 worker + 并行 apply worker,留 20% 余量。
  4. 同版本族订阅才开 binary。 跨大版本迁移链路保持文本格式;同版本高频同步(全量 + 增量)开启 binary 减少编解码开销。
  5. 权限模型按 PG16 默认收紧,不回退。 除非有明确的老系统兼容需求,不设 run_as_owner = true;订阅 owner 走专用角色 + pg_create_subscription 授权,不使用超级用户。
  6. 级联/环形拓扑统一 origin = none。 让「只转发本地变更」成为拓扑铁律,配合复制原点监控回环;双向同步场景单独评估(需要双向则两端都不设 none 并做冲突治理)。
  7. 无主键表两端口径一致。 REPLICA IDENTITY FULL + 订阅端对等索引一起变更;能补主键就补主键(首选方案,见逻辑复制原理的复制标识一节)。
  8. 订阅端重试节奏按业务分级。 实时订阅短间隔、汇总订阅长间隔,用订阅级 wal_retrieve_retry_interval 替代全局参数。
  9. 监控三件套常态化。 pg_stat_subscription(worker/进度/leader_pid)、pg_stat_subscription_stats(错误计数)、pg_replication_slots(备库视角的 WAL 保留),三条告警缺一不可。
  10. 升级 PG16 后对存量订阅做一轮回归。 逐个检查 run_as_owner 权限链、worker 总量、streaming 取值与统计条目是否健康;PG15→16 的行为变化集中在权限与并行,回归成本低但收益高。
  11. walsender 排障用进程标题快速定位。 多库主库上 ps 直接看数据库名,定位后再进 pg_stat_replication 拿细节,减少一层查询。

下一节

继续学习:PG16:VACUUM 与性能改进

参考链接 ​