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 引入备库逻辑解码后,逻辑解码可以在热备库上运行,典型新拓扑:
物理流复制 逻辑复制
主库 ────────────────► 备库 ────────────────► 下游订阅端
(业务写入) (逻辑发布/解码) (汇聚/分析)发布、订阅与回放的完整模型回顾见逻辑复制原理,订阅端选项的完整清单见订阅(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 | 发布端 | 10 | walsender 上限(PG16 默认未变) |
max_worker_processes | 订阅端 | 8 | 后台 worker 总池 |
max_logical_replication_workers | 订阅端 | 4 | apply / 同步 / 并行 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 申请失败后反复重试。
语法与用法
备库逻辑解码的启用与验证
# 备库 postgresql.conf:逻辑解码的前提与主库一致
wal_level = logical
max_replication_slots = 10备库上创建逻辑槽时,PG16 提供了一个配套函数解决「备库无法自发产生快照 WAL 记录」的初始化问题:
-- 在备库执行:写入一条 standby 快照 WAL 记录,允许逻辑槽创建
SELECT pg_log_standby_snapshot();
-- 随后正常创建逻辑槽
SELECT * FROM pg_create_logical_replication_slot('pub_slot', 'pgoutput');验证解码在备库上工作:
# 在备库消费解码流
pg_recvlogical -h standby-host -d app --slot pub_slot --start -f -主库上做一次 INSERT,备库的解码流应输出对应的逻辑变更。备库的只读属性对发布端 walsender 无影响——解码是「读 WAL」,不是「写数据」。
并行应用:创建订阅与参数
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 的取值
);# 订阅端 postgresql.conf
max_parallel_apply_workers_per_subscription = 4 -- 新增于 PG16
max_logical_replication_workers = 8
max_worker_processes = 16 -- 所有 worker 的总量池观察并行 apply:
-- 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 把二进制格式扩展到了初始表同步阶段:
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 身份执行)通过选项保留:
CREATE SUBSCRIPTION sub_orders
CONNECTION '...'
PUBLICATION pub_orders
WITH (run_as_owner = true); -- 恢复 PG15 行为配套的约束:订阅 owner 必须是超级用户,或对所有相关表属主具备 SET ROLE 权限(否则创建订阅报错)。
pg_create_subscription 预定义角色与免密码连接
-- 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 选项与回环防护
-- 只应用「无来源标记」的本地变更:级联/环形拓扑防回环(新增于 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 预加载 + 增量订阅:
# 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-- 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 以内走内置同步省心;大表 / 多表长窗口走预加载,把「追赶增量」的时间从天级压到小时级。
增量应用的顺序与可见性保证
订阅端回放遵循两条不变式,理解它们才能正确设计下游:
- 事务边界保持:发布端的一个事务在订阅端也是一个事务,要么全部应用、要么全部不应用;并行 apply 的并行范围只发生在「事务内部」的子事务之间,跨事务的提交顺序不变;
- 按提交顺序应用:leader apply worker 按发布端的提交顺序分发,下游看到的行状态序列与发布端一致。
由此的推论:
- 订阅端延迟曲线上的「尖峰」来自大事务(提交后才整体可见),
streaming = parallel缩短的是大事务的回放时长,不改变可见性时点; - 长事务提交前的变更不发布(逻辑解码在提交时才输出完整事务),发布端的超长事务会推迟其后所有增量;
- 应用侧不应假设「事务内的行级变更按行序到达下游」,只应依赖事务边界——这与单机复制语义一致;
- 流式发送中(
streaming开启)的大事务在提交前对订阅端查询不可见,不要期望「实时读到大事务的中间状态」。
run_as_owner 前后的权限对照
| 执行者 | PG15 | PG16 默认 | 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 索引(按旧值等值查找)代替顺序扫描:
-- 发布端:无主键表 + 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 起改为订阅级选项,创建订阅时指定:
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 清单
-- 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 |
| 备库发布的槽在主库看不到 | 槽位于备库 | 到备库端查询与维护 |
-- 事务级跳过(订阅端):定位到报错事务的 finish LSN 后
ALTER SUBSCRIPTION sub_orders SKIP (lsn = '0/2A0B1C00');SKIP 只应作为「已确认该事务可丢弃」的最后手段——跳过的变更不会重放。先在 streaming = on 模式下复现拿到完整错误(并行模式下的 finish LSN 可能缺失,见注意事项),再决定跳过还是修复数据。
实战示例
示例一:备库发布 + 双端验证
# 主库:确保 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# 备库:消费解码流(观察窗口)
pg_recvlogical -h standby-host -d app --slot pub_slot --start -f - &# 主库:写入变更
psql -h primary-host -U postgres -d app \
-c "INSERT INTO public.orders (id, amount) VALUES (9001, 199) ON CONFLICT (id) DO NOTHING;"pg_recvlogical 输出:
table public.orders: INSERT: id[bigint]:9001 amount[numeric]:199清理演示槽:SELECT pg_drop_replication_slot('pub_slot');(在备库执行)。
示例二:并行 apply 的大事务订阅
# 发布端:建发布(原理见 publication 页)
psql -h primary-host -U postgres -d app -c \
"CREATE PUBLICATION pub_events FOR TABLE public.events;"-- 订阅端
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);-- 订阅端观察并行 worker 的启动与状态流转
SELECT subname, worker_type, pid, leader_pid, received_lsn, latest_end_lsn
FROM pg_stat_subscription ORDER BY subname; 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 防回环
主库 A ──逻辑复制──► 汇聚库 B ──逻辑复制──► 归档库 C-- 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 预加载 + 免停机增量接管
# 发布端:建发布
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-- 3) 订阅端:跳过初始拷贝,只接增量
CREATE SUBSCRIPTION sub_orders
CONNECTION 'host=publisher dbname=app user=repl password=secret'
PUBLICATION pub_orders
WITH (copy_data = false);-- 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 依然如此),接管前要把订阅端序列拨到发布端当前值。
示例五:多对一汇聚拓扑
把两个区域的订单汇聚到中央分析库(多对一是逻辑复制的经典形态):
-- 中央库(订阅端):两个订阅分别指向两个区域
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);-- 汇聚端观察:两个订阅各自独立计量
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、错误与决策依据,进入变更记录;无痕跳过攒多了,两端数据的一致性就再也说不清了。
最佳实践
- 发布端尽量选备库,主库只留业务写入。 PG16 的备库逻辑解码让「发布不抢主库 CPU」从设计愿望变成默认拓扑;主备切换的订阅重建预案要预先演练。
- 大事务订阅显式启用
streaming = parallel。 小事务场景保持off/on即可;并行度从max_parallel_apply_workers_per_subscription = 2起步压测,不要一步到位拉高。 max_worker_processes总账先行。 物理 worker 池 = 物理 worker 常规占用 + 逻辑复制 worker + 同步 worker + 并行 apply worker,留 20% 余量。- 同版本族订阅才开
binary。 跨大版本迁移链路保持文本格式;同版本高频同步(全量 + 增量)开启 binary 减少编解码开销。 - 权限模型按 PG16 默认收紧,不回退。 除非有明确的老系统兼容需求,不设
run_as_owner = true;订阅 owner 走专用角色 +pg_create_subscription授权,不使用超级用户。 - 级联/环形拓扑统一
origin = none。 让「只转发本地变更」成为拓扑铁律,配合复制原点监控回环;双向同步场景单独评估(需要双向则两端都不设 none 并做冲突治理)。 - 无主键表两端口径一致。
REPLICA IDENTITY FULL+ 订阅端对等索引一起变更;能补主键就补主键(首选方案,见逻辑复制原理的复制标识一节)。 - 订阅端重试节奏按业务分级。 实时订阅短间隔、汇总订阅长间隔,用订阅级
wal_retrieve_retry_interval替代全局参数。 - 监控三件套常态化。
pg_stat_subscription(worker/进度/leader_pid)、pg_stat_subscription_stats(错误计数)、pg_replication_slots(备库视角的 WAL 保留),三条告警缺一不可。 - 升级 PG16 后对存量订阅做一轮回归。 逐个检查 run_as_owner 权限链、worker 总量、streaming 取值与统计条目是否健康;PG15→16 的行为变化集中在权限与并行,回归成本低但收益高。
- walsender 排障用进程标题快速定位。 多库主库上
ps直接看数据库名,定位后再进pg_stat_replication拿细节,减少一层查询。
下一节
继续学习:PG16:VACUUM 与性能改进
参考链接
- PostgreSQL 16 Release Notes
- PostgreSQL 16 官方文档:逻辑复制
- PostgreSQL 16 官方文档:CREATE SUBSCRIPTION
- PostgreSQL 16 官方文档:预定义角色(pg_create_subscription)
- PostgreSQL 16 官方文档:逻辑解码
- PostgreSQL 16 官方文档:pg_stat_subscription
- PostgreSQL 16 官方文档:复制配置(worker 相关 GUC)
- PostgreSQL 16 官方文档:逻辑复制协议
- PostgreSQL 16 官方文档:pg_replication_slots
- PostgreSQL 16 官方文档:REPLICA IDENTITY