
1. 先搞清楚逻辑复制解决什么问题以及何时不该用它1.1 物理复制搞不定的事情很多刚接触 PostgreSQL 18 逻辑复制的人第一个问题往往是已经有了流复制物理复制为什么还要逻辑复制这个问题问得非常关键因为它直接决定了你是否需要在这个功能上投入精力。物理复制的工作方式是直接把 WAL 日志里的数据页变更原封不动地在备库上重放。这意味着主库和备库的版本必须完全一致数据块层面的字节必须对齐备库只能作为整库的只读副本存在。你没有办法只复制其中几张表没有办法让备库承担写入更没有办法在不同大版本之间做持续同步。这些限制在日常运维中会变成很现实的痛点。举个实际场景假设你有一套 PostgreSQL 12 的订单库计划升级到 PostgreSQL 18。物理复制在这里完全帮不上忙因为 PostgreSQL 12 和 18 的数据目录格式不兼容物理备库根本无法启动。你只能选择停机迁移、pg_dump 导出导入或者用逻辑复制搭一条增量同步链路让新老版本并行运行一段时间再切换。后者是目前生产环境里最稳妥、也是我强烈推荐的做法。1.2 到底该在哪些场景用逻辑复制基于我这些年在一线运维和迁移项目里的经验逻辑复制真正发挥价值的主要有四类场景。第一类是跨大版本升级。这是最典型的应用。老版本库作为发布端新版本库作为订阅端数据持续同步业务在切换窗口内只产生极小甚至为零的停机时间。等新库数据追平后把应用连接切过去再停掉复制链路即可。第二类是部分表的数据分发。比如线上交易库里的订单表和用户表需要实时同步到另一个分析库或报表库但又不希望把日志表、临时表这些无关数据一起带过去。逻辑复制可以按表、按行、按列精确控制复制范围物理复制做不到这一点。第三类是多库汇聚或数据中台场景。多个业务库各自发布一部分表汇入同一个中心库做统一加工。每个源库独立发布、独立订阅互不干扰订阅端还能通过表名前缀区分数据来源。第四类是异构消费。逻辑复制的本质是把 WAL 翻译成行级变更流INSERT、UPDATE、DELETE下游不一定是 PostgreSQL可以是 Kafka、ClickHouse 这类消费端。虽然这通常需要额外的第三方工具但逻辑复制本身就是这一体系的基石。1.3 一条原则能用物理复制就别用逻辑复制这句话听起来有点劝退但我要先把这个原则讲清楚否则你很容易在错误的场景里被逻辑复制坑得很惨。逻辑复制的代价比大多数人想象的要高。它需要在发布端开启逻辑解码WAL 里需要保留足够多的信息解码过程本身会消耗 CPU订阅端每张复制表都会有一个 apply worker 在跑遇到大事务时内存和临时文件的开销也不小更麻烦的是逻辑复制只同步 DML 数据变更不同步表结构变更发布端执行一个ALTER TABLE ADD COLUMN订阅端不会自动跟着加列处理不好直接导致复制中断。所以如果目标只是同一版本 PostgreSQL 的整库读写分离、高可用容灾老老实实用物理流复制就好它开销更低、运维更成熟、行为更可预期。逻辑复制适合的是物理复制解决不了的那些问题。想清楚这一点后面所有配置和排错才有意义。2. 逻辑复制跑起来的完整链路哪位在干什么2.1 一条 UPDATE 从发布端开始理解逻辑复制最关键的是把它和物理复制区分开。物理复制的本质是数据页级的重放逻辑复制的本质则是WAL 翻译 行级回放。一条 UPDATE 在发布端执行后照常写入 WAL。数据库里有一个叫walsender的进程负责把变更发送给订阅端而逻辑复制场景下walsender 不是直接传输 WAL 原始字节而是调用一个输出插件把 WAL 里的变更解析成逻辑变更流。PostgreSQL 默认自带的输出插件是pgoutput它会根据发布Publication的配置把 WAL 中与该表相关的变更翻译成第几行的哪几个列被更新成什么值这样的逻辑消息再通过网络协议发送出去。订阅端收到消息后由 apply worker 进程负责在本地执行对应的 INSERT、UPDATE、DELETE。整个过程你可以理解为发布端说的不是第 12345 个数据页的偏移量 678 被改成了某个二进制值而是订单表 id1000 的那行金额列改成了 99.9。后者不依赖物理存储格式所以天然能跨版本工作。2.2 Publication 不是表是筛选规则很多初学者会把发布理解成一张表的快照或者一个任务其实它是数据库里的一个逻辑对象本质上是一套筛选规则。创建发布的时候你可以选择FOR ALL TABLES也可以选择FOR TABLE 指定表。PostgreSQL 15 之后还支持按 Schema 发布比如FOR TABLES IN SCHEMA public。从 15 开始支持行过滤发布某张表时可以加 WHERE 条件从 16 开始支持列过滤可以只发布指定的列。创建发布时还可以通过publish参数控制复制哪些操作类型默认是insert, update, delete, truncate。-- 发布表 orders只同步金额大于 100 的行且只同步 id、amount、created_at 三列 CREATE PUBLICATION app_pub FOR TABLE orders WHERE (amount 100) WITH (publish insert, update, delete, truncate);这个示例如果你在 PostgreSQL 15 之前的环境里跑WHERE子句是不被支持的如果在 16 之前跑列过滤也不支持。所以务必确认两端的版本能力。另外要特别提醒发布只负责数据的变更不负责表结构。发布端CREATE TABLE、ALTER TABLE这类 DDL 完全不会同步到订阅端订阅端的表结构需要提前手动准备好。2.3 复制槽逻辑复制的命脉复制槽可能是整个逻辑复制链路里最需要敬畏的组件。每个订阅在发布端都会占用一个逻辑复制槽logical replication slot它记录着当前解码已经推进到了哪条 WAL 记录。所有 WAL 日志在 PostgreSQL 里是有编号的复制槽会保存两个关键位置restart_lsn表示解码器必须保留的最早 WAL 位置confirmed_flush_lsn表示订阅端已经确认消费到的位置。如果订阅端长期离线WAL 会一直保留在发布端直到复制槽被删除。这也是逻辑复制最危险的地方一个忘记清理的死掉的订阅可以把发布端的磁盘撑爆因为 WAL 永远不会被回收。这也是为什么生产环境里必须对复制槽做监控。后面我会专门讲监控手段这里先记住结论逻辑复制槽是命脉不能随手删更不能放任不管。初始同步阶段复制槽还承担着一致性保证的任务。CREATE SUBSCRIPTION在建槽时发布端会基于一个一致性快照导出表数据同时记录下快照对应的 LSN 位置。之后订阅端一边 COPY 历史数据一边从该 LSN 继续接收增量变更从而保证初始数据 增量数据不重不漏逻辑上一致。2.4 订阅端 worker 的构成订阅端的结构相对简单但也有一些值得理解的细节。每个启用的订阅会至少有一个 apply worker 进程它负责连接发布端、拉取变更、在本地执行。PostgreSQL 14 开始支持流式事务大事务不必等提交后才统一发送而是可以边产生边传输内存压力显著降低PostgreSQL 16 开始支持并行 apply一个订阅可以在多个 worker 之间并行回放不同表的事务提升大事务场景的吞吐。我在实际使用中有一条经验虽然并行 apply 听起来很美好但它对表之间的外键关系、对同一行并发更新这类场景并没有你做主。如果订阅端表之间有复杂外键或者业务对同一行的更新频率极高建议先保持默认并行度跑一段时间观察冲突率再做调整不要一上来就追求最大并行数。3. PostgreSQL 18 给逻辑复制带来的两个看得见的变化3.1 pg_createsubscriber把物理备库无缝转成逻辑订阅端如果说 PostgreSQL 18 的逻辑复制有一个最值得关注的工具那一定是pg_createsubscriber。它是 PG18 新增的 contrib 工具解决的问题非常具体如何把一个已经在跑的物理备库平滑地转换成一个逻辑复制的订阅端。为什么需要这个能力考虑一个跨版本升级的典型路径你有一主一备的 PostgreSQL 16 集群想升级到 18。物理复制只能做到 16 对 16跨版本必须改用逻辑复制。可问题是你的备库上已经同步了一大堆数据如果直接把它降级为普通库、再从发布端重新做一次全量初始同步等于白白浪费了备库上已有的数据数据量大的时候这个代价非常痛苦。pg_createsubscriber的思路就是既然备库上的数据已经追平了主库让它保留现有数据断开物理复制然后在本地直接建立到新版本发布端的逻辑订阅。由于数据已经是最新的这个订阅甚至不需要再做全量 COPY只需要从某个断点开始继续接收增量即可。整个过程把原本可能要重做全量同步的操作压缩成了一次快速切换。需要提醒的是这个工具的使用场景有严格前提备库必须和主库数据保持同步转换过程需要短暂停止复制操作前务必确认备份完整。我第一次在测试环境跑通这个流程时最大的感受是它确实把物理复制拓扑平滑升级到逻辑复制拓扑这件事变得真正可操作了而不需要再依赖纯手工的 pg_dump 加重建订阅。3.2 ad hoc logical replication物理与逻辑拓扑混用的解药PostgreSQL 18 里另一个与逻辑复制相关的方向是 ad hoc logical replication中文社区一般翻译成即席逻辑复制。它解决的是一个此前容易被忽视、但真实存在的拓扑冲突问题。过去如果你在一个集群上既配置了物理复制备库拉 WAL又配置了逻辑复制从同一节点做逻辑发布两种复制机制对 WAL 保留的要求可能会互相干扰。物理备库要求主库保留足够多的 WAL 以便追平逻辑复制槽也要求保留对应区间的 WAL两条保留需求叠加在一块一旦某个环节推进不及时可能出现物理复制或逻辑复制被迫中断的尴尬局面。这也是为什么很多老 DBA 会要求生产环境把两种复制的源节点分开部署。PG18 的 ad hoc logical replication正是针对这种混合拓扑做了改进让物理复制和逻辑复制可以在同一套拓扑里更灵活地共存。它并不是一个简单的开关而是对逻辑解码和 WAL 保留机制做了更精细的协同管理减少了两种复制方式互相踩踏的可能。再配合 PostgreSQL 17 已经支持的在备库上进行逻辑解码能力升级和迁移链路的部署方式会灵活很多。如果你只是搭一条简单的单库逻辑复制链路这个特性未必直接用到但如果你的架构设计里既有备库又有下游消费端PG18 的这一版变化值得你在画拓扑图的时候多想一步。3.3 别把版本特性记混了逻辑复制的功能是分版本逐步叠加的很多博客把不同版本的功能混在一起讲导致排查问题时定位错原因。我整理了一张简表按版本把关键能力列清楚你在规划迁移或阅读官方文档时对照着看会少走弯路。版本逻辑复制相关的重要变化PG 10逻辑复制正式引入发布/订阅模型可用PG 13支持并行初始同步多表 COPY 可并行执行PG 14支持流式事务传输大事务不再等提交才发送PG 15支持行过滤WHERE 条件、ALTER SUBSCRIPTION ... SKIPPG 16支持列过滤、并行 apply、初始同步默认使用二进制格式PG 17支持在物理备库上进行逻辑解码实现方式更灵活PG 18新增pg_createsubscriber完善 ad hoc logical replication 等混合拓扑能力有了这张表你在阅读报错或者选择语法时就能先判断一下这个功能我的版本到底支不支持而不是拿着高版本的语法去低版本环境里空跑一遍。4. 实战用 PostgreSQL 18 搭一条跨版本逻辑复制链路4.1 环境与参数wal_level 是起点理论讲再多不如实际动手跑一遍。下面我带你把一套PostgreSQL 18 发布端 PostgreSQL 16 订阅端的链路完整搭起来这也正好模拟了跨版本升级时最常见的形态。假设环境如下发布端pg18-primaryPostgreSQL 18IP10.0.1.10:5432业务库appdb订阅端pg16-subPostgreSQL 16IP10.0.1.20:5432目标库appdb首先修改发布端的postgresql.conf。以下参数是逻辑复制的底线配置wal_level logical max_replication_slots 20 max_logical_replication_workers 8 max_worker_processes 16wal_level必须至少是logical。需要注意这个参数修改后必须重启PostgreSQL 才会生效max_replication_slots、max_logical_replication_workers也一样。max_worker_processes是所有后台 worker 的总预算逻辑复制的 apply worker 会占用其中的名额所以也不要设得太小。订阅端理论上不需要开启逻辑解码但建议把max_logical_replication_workers也设置成合适的值因为 apply worker 就运行在订阅端。4.2 发布端配置用户、发布对象与过滤规则发布端需要一个专门用于复制的账号。它必须具备REPLICATION权限用于建立复制连接和读取发布表的权限用于初始同步时的数据读取。实际生产里我不会直接给超级用户而是建一个独立账号。-- 在发布端执行 CREATE ROLE repuser LOGIN REPLICATION PASSWORD strong_password; GRANT CONNECT ON DATABASE appdb TO repuser; GRANT USAGE ON SCHEMA public TO repuser; GRANT SELECT ON orders, users, order_items TO repuser;接着创建发布。这里我做一个小设计orders表全量发布users表带行过滤order_items表做列过滤方便你看到不同过滤规则同时存在时怎么组织。CREATE PUBLICATION app_pub FOR TABLE orders, users WHERE (id 1000), order_items (id, order_id, amount) WITH (publish insert, update, delete, truncate);注意FOR TABLE后面每一张表都可以单独加 WHERE 或列清单这是 PostgreSQL 15 之后逐步支持的能力。订阅端接收到的数据只会包含这些过滤后的结果这也意味着发布端和订阅端的数据集从逻辑上就不再是等价的查问题的时候要心里有数。4.3 订阅端配置一条命令完成的初始同步在创建订阅之前有一步千万不能省把表结构先建到订阅端。逻辑复制不会同步 DDL所以先用pg_dump只导出结构导入到订阅端。# 在发布端导出结构 pg_dump -h 10.0.1.10 -U repuser -d appdb --schema-only -f appdb_schema.sql # 导入到订阅端 psql -h 10.0.1.20 -U postgres -d appdb -f appdb_schema.sql然后创建订阅-- 在订阅端执行 CREATE SUBSCRIPTION app_sub CONNECTION host10.0.1.10 port5432 dbnameappdb userrepuser passwordstrong_password PUBLICATION app_pub WITH (create_slot true, slot_name app_sub_slot, enabled true);执行这条命令后订阅端会自动连接发布端创建逻辑复制槽app_sub_slot对三张表做初始全量 COPY然后持续接收增量。如果表数据量很大这个过程会持续一段时间期间可以通过pg_stat_progress_copy观察 COPY 进度。这里有个细节如果业务允许短暂停机最稳妥的顺序是先用pg_dump导出结构和数据、导入订阅端再创建不带初始同步的订阅然后开启复制。但要注意这中间存在时间差必须清楚结构 存量数据 复制增量三者如何衔接不要引入数据空洞。4.4 验证复制真的在跑链路搭完验证两步就够了。第一步看订阅端系统视图SELECT subname, pid, received_lsn, latest_end_lsn, last_msg_receipt_time FROM pg_stat_subscription;如果pid有值说明 apply worker 在运行received_lsn和latest_end_lsn在持续增长说明增量在推进。第二步在发布端插入一条测试数据立刻到订阅端查询-- 发布端 INSERT INTO orders (id, amount, created_at) VALUES (10001, 199.9, now()); -- 订阅端 SELECT * FROM orders WHERE id 10001;能看到这条记录链路就算通了。接下来再试试 UPDATE 和 DELETE确保三种操作类型都同步正常。4.5 为什么建议把初始同步交给 CREATE SUBSCRIPTION我见过很多团队会先手动把数据从发布端导出、导入订阅端再创建订阅。这样做当然可以但有一个巨大的隐患你手工导数据的过程里发布端的数据可能一直在变。如果先导数据、后建订阅建订阅时复制槽只能从那个时间点开始记录增量中间这段时间的变更就丢了。CREATE SUBSCRIPTION自动初始同步之所以更可靠是因为它借助逻辑复制槽的一致性快照机制把存量数据快照和增量起始点绑定在同一个时刻。快照开始 COPY 时记录的 LSN决定了后续解码从哪里继续因此数据不重不漏。除非表太大、COPY 时间不可接受我强烈建议优先用自动初始同步。如果表实在太大也可以考虑用发布端pg_dump -Fd配合并行导出工具先迁移存量再配置复制槽但这时候你必须自己卡尔好快照点和增量起点否则丢数据的概率极高。这种方案我只建议有经验的 DBA 在严格控制窗口的场景下使用。5. 日常运维中最容易翻车的四个点5.1 新增表没有进入发布链路跑通后最常见的问题就是发布端建了一张新表逻辑复制却完全没有反应。原因多半是发布对象的范围没覆盖到。如果你创建发布时用的是FOR TABLE那么后续新建的表默认不会被自动包含。FOR TABLES IN SCHEMA也有类似限制它并不会动态追踪该 Schema 下的新表。只有FOR ALL TABLES发布才会自动包含之后新建的表。所以每次上线新表记得手动把表加进发布ALTER PUBLICATION app_pub ADD TABLE new_table;同时订阅端要提前建好对应的表结构否则 apply worker 会因为目标表不存在而报错。5.2 结构变更不会自动跟随这条前面反复强调但值得再展开一次。发布端执行ALTER TABLE orders ADD COLUMN discount numeric(8,2)订阅端不会自动加这列于是紧接着而来的 UPDATE 很可能在订阅端执行失败因为语句里包含了订阅端上不存在的列。正确的处理顺序是先改订阅端再改发布端。先在订阅端执行ALTER TABLE ... ADD COLUMN等两端结构一致后再在发布端执行同样的 DDL。为什么先下游后上游因为逻辑复制的数据流是从上游到下游只要下游先把结构准备好上游再变更时增量数据就能正常落库。如果你管理的表很多强烈建议写一个小脚本专门记录两端表结构差异发布变更前置检查时跑一遍。逻辑复制不负责 DDL 同步这是它相对于某些商业数据库复制工具最大的短板只能靠流程去补。5.3 订阅卡死主键冲突和数据不一致逻辑复制最常见的卡死原因是主键冲突。典型场景是订单表在主键列用序列自增由于序列不会复制订阅端自己生成的序列值和发布端不一致某条 INSERT 在订阅端撞了唯一约束apply worker 报错并停止。日志里通常会看到类似这样的错误ERROR: duplicate key value violates unique constraint orders_pkey。这时候需要人工介入。处理思路是先确认问题边界。如果只是单条冲突可以使用ALTER SUBSCRIPTION ... SKIP (lsn 具体的LSN)跳过这条事务如果多条冲突或数据本身已经不一致不要盲目 SKIP而是要把两端数据核对清楚必要时重建订阅。-- 跳过指定 LSN 处的事务 ALTER SUBSCRIPTION app_sub SKIP (lsn 0/16B5C10);SKIP 是一个很有用的运维手段但它只能跳过当前阻塞的事务不会修复历史数据问题。我的原则是能修复数据就先修复数据SKIP 是最后手段并且每次跳过都要记录原因避免把真正的数据问题掩盖掉。5.4 序列和自增列最容易被忽略的隐患序列不同步是逻辑复制最经典的隐藏坑。PostgreSQL 的SERIAL或IDENTITY列底层依赖的 sequence 不会通过逻辑复制同步所以即使数据同步正常序列值也会各走各的。比如发布端INSERT后订单 ID 是 50001订阅端因为没收到这个序列变化下次自己插入时序列还是从 1 开始很快就会撞主键。解决方案通常有这么几种主键不要用序列改用应用生成的 UUID在发布端和订阅端把 sequence 的起始值错开避免撞值定期把发布端的序列当前值同步到订阅端第三种是最常见的落地手段。PG18 环境里你可以写一个小函数遍历所有表的序列把last_value从发布端更新到订阅端在业务低峰期执行。这里不展开完整代码但思路就是这个思路。只要用序列做主键这个问题就绕不开别等到撞了才发现。6. 监控逻辑复制我日常盯的几个指标6.1 发布端看 slot 和 WAL逻辑复制链路健康与否发布端上市最有信息量的是复制槽视图。我日常会用下面这条 SQLSELECT slot_name, plugin, slot_type, active, restart_lsn, confirmed_flush_lsn, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS wal_retained FROM pg_replication_slots WHERE slot_type logical;重点关注两列active是否为 true以及wal_retained有多大。如果复制槽是 active 的说明订阅端在线消费如果wal_retained持续增大说明订阅端消费速度跟不上产生速度时间长了你可能要考虑升级发布端磁盘或检查订阅端性能。逻辑复制槽如果长期不活跃发布端的 WAL 会一直堆积。我见过不止一次因为忘记删除废弃订阅导致发布端磁盘被 WAL 撑爆的事故。所以一定要对非 active 的复制槽设置告警超过阈值就人工确认是否清理。6.2 订阅端看 worker 状态订阅端主要看两个视图。一个是pg_stat_subscription看 apply worker 是否存活、LSN 是否前进SELECT subname, pid, relid, received_lsn, latest_end_lsn, last_msg_send_time, last_msg_receipt_time, last_report_time FROM pg_stat_subscription;如果pid为空说明 worker 已经退出链路大概率中断了。这时候去 PostgreSQL 日志里搜logical replication apply worker相关的报错多半能找到具体原因。另一个视图是 PostgreSQL 16 引入的pg_stat_subscription_stats它统计了订阅端的 apply 冲突次数和其他工作指标。通过观察apply_error_count的增长趋势可以在问题还没完全卡死时提前发现风险。6.3 用心跳表量化端到端延迟上面的 LSN 视图只能说明复制进程在工作但无法精确反映业务写入到订阅端可见经过了多少时间。如果要量化端到端延迟我习惯的做法是建一张心跳表。发布端和订阅端都建同一张表CREATE TABLE heartbeat ( id int PRIMARY KEY, ts timestamptz NOT NULL DEFAULT now() );发布端每隔 1 秒往表里写入或更新一条记录INSERT INTO heartbeat (id, ts) VALUES (1, now());订阅端延迟 now() - 心跳表里最新一条 tsSELECT now() - ts AS delay FROM heartbeat ORDER BY id DESC LIMIT 1;这个方案非常简单却非常实用。它测到的不只是解码和传输延迟还包括 apply worker 落库之后的时间是真正业务视角的端到端延迟。我用这个方式给很多客户搭过逻辑复制延迟监控效果稳定你完全可以直接照抄。如果业务表本身有更新时间字段也可以直接拿一张高频写入的业务表来算延迟效果类似但要注意业务表可能有大事务或者批量更新算出来的延迟会有毛刺不如心跳表干净。最后说点个人体会。逻辑复制这个功能名字里带复制但它的定位更像数据集成工具——它能跨版本、能按表按行按列筛选、能多对一汇聚但同时也要求你对表结构、序列、复制槽这些基础设施保持敬畏。PostgreSQL 18 里pg_createsubscriber和 ad hoc logical replication 的加入让它在升级和复杂拓扑场景下明显更顺手了但底层的那些坑——DDL 不同步、序列不复制、槽位管理——并不会自动消失。我建议你如果准备上 PG18 的逻辑复制先从一条业务旁路开始练手把发布、订阅、监控、跳过冲突这套操作走顺了再考虑把它架到核心链路上。毕竟这类工具出问题的时候最宝贵的不是修复手段而是你能多快定位到问题在哪一环。