3. 发布与订阅模型详解

PostgreSQL 的逻辑复制采用“发布—订阅”模型:发布端(Publisher)把某些表的变更以逻辑解码的方式暴露出来,订阅端(Subscriber)通过一个复制连接持续拉取这些变更并重放。理解这一模型的关键,是把“发布定义了什么”“订阅如何连接”“复制槽由谁创建和维护”这三件事分开看,再合起来看它们如何协同。

发布端:CREATE PUBLICATION 定义“发布什么”

发布对象由 CREATE PUBLICATION 定义。它本身不建立连接,也不启动复制,只是声明一组表以及这些表的变更类型(INSERT/UPDATE/DELETE/TRUNCATE)是否对外发布。基本语法如下:

CREATE PUBLICATION pub_name
    FOR TABLE t1, t2
    | FOR ALL TABLES
    | FOR TABLES IN SCHEMA s1, s2
    WITH (publish = 'insert, update, delete, truncate');

发布对象有三种粒度:表级(FOR TABLE)、模式级(FOR TABLES IN SCHEMA,PostgreSQL 15 起支持)、全库级(FOR ALL TABLES)。表级最精确,适合只同步少量核心表;模式级适合按业务模块批量发布;全库级最省事,但会把后续新建的表也纳入发布范围,需要评估对订阅端的影响。

publish 参数控制发布哪些操作类型,默认是 insert, update, delete, truncate。如果订阅端只需要增量数据、不需要同步删除,可以写成 publish = 'insert, update'。此外还有 publish_via_partition_root 参数:当发布的是分区表时,设为 true 会让变更以根表身份发布,订阅端只需存在根表即可,避免逐分区维护。

发布端还需要保证 wal_level = logical,否则逻辑解码无法工作。发布定义本身不占用复制槽,复制槽是在订阅端创建订阅时才建立的。

订阅端:CREATE SUBSCRIPTION 定义“怎么连、连什么”

订阅由 CREATE SUBSCRIPTION 定义,它同时承担两件事:声明订阅哪个发布,以及建立到发布端的复制连接。基本语法如下:

CREATE SUBSCRIPTION sub_name
    CONNECTION 'host=192.168.1.10 port=5432 dbname=app user=repl password=***'
    PUBLICATION pub_name
    WITH (copy_data = true, create_slot = true, enabled = true);

CONNECTION 是一个 libpq 连接串,常用参数包括 host、port、dbname、user、password、sslmode。这里的 dbname 必须是发布端上承载发布的数据库,user 需要具备复制权限(REPLICATION 或超级用户),并且能在发布端读取相应表的初始数据。

PUBLICATION 可以一次指定多个发布,订阅端会把它们合并成一条复制流。关键参数包括:

  • copy_data:默认 true,表示订阅建立时先对已发布表做一次初始数据拷贝(类似逻辑快照),之后再进入增量同步。若订阅端已有全量数据,可设为 false 只接收增量。
  • create_slot:默认 true,表示由订阅端自动在发布端创建复制槽。若发布端不允许订阅端建槽,可设为 false 并手工建槽。
  • enabled:默认 true,设为 false 时订阅创建后不立即启动,便于先检查再启用。
  • slot_name:指定复制槽名称,默认与订阅名相同。

复制槽的自动创建与管理

逻辑复制槽是发布端上的一个持久对象,它记录“已经发送到哪个 WAL 位置”,从而保证订阅端断线重连后不会丢数据,也防止发布端过早回收仍需要的 WAL。当 create_slot = true 时,CREATE SUBSCRIPTION 会在发布端自动执行类似 pg_create_logical_replication_slot('sub_name', 'pgoutput') 的动作,并使用 pgoutput 输出插件。

复制槽的管理要点:

  • 查看:在发布端执行 SELECT * FROM pg_replication_slots;,关注 slot_name、plugin、active、restart_lsn、confirmed_flush_lsn。
  • 删除:订阅端执行 DROP SUBSCRIPTION 时,若 create_slot = true,会尝试一并删除发布端复制槽;若发布端不可达,需要先在发布端手工 SELECT pg_drop_replication_slot('sub_name');,再在订阅端用 ALTER SUBSCRIPTION ... SET (slot_name = NONE) 解绑后删除。
  • 风险:复制槽会阻止 WAL 被回收。如果订阅端长期停止且槽仍存在,发布端 pg_wal 会持续增长,可能撑满磁盘。因此停用订阅时应评估是否保留槽。
复制槽是“发布端资源、订阅端触发创建”。排查同步中断时,先看订阅端 pg_stat_subscription 的 last_msg_send_time 与 last_msg_receipt_time,再回到发布端看复制槽的 active 与 restart_lsn 是否推进。
发布端 Publisher wal_level = logical CREATE PUBLICATION pub_app FOR TABLE orders, customers publish = 'insert, update, delete' 发布对象粒度 表 / 模式 / 全部表 逻辑复制槽 slot: sub_app plugin = pgoutput restart_lsn / confirmed_flush_lsn 保留 WAL,防止过早回收 由订阅端 create_slot=true 自动创建 订阅端 Subscriber 接收并重放变更 CREATE SUBSCRIPTION sub_app CONNECTION 'host=... dbname=app' PUBLICATION pub_app copy_data=true, create_slot=true 初始数据拷贝 copy_data=true 时先做快照同步 之后进入增量重放 订阅状态观测 pg_stat_subscription last_msg_send / receipt_time 逻辑解码输出 创建/确认复制槽 发布定义“发布什么”,订阅定义“怎么连、连什么”,复制槽保证断点续传与 WAL 保留

一次完整的建立过程与观察点

假设发布端 192.168.1.10 上已有 app 库,需要把 orders、customers 同步到订阅端。发布端先执行:

CREATE PUBLICATION pub_app
    FOR TABLE orders, customers
    WITH (publish = 'insert, update, delete');

订阅端执行:

CREATE SUBSCRIPTION sub_app
    CONNECTION 'host=192.168.1.10 port=5432 dbname=app user=repl password=secret sslmode=prefer'
    PUBLICATION pub_app
    WITH (copy_data = true, create_slot = true, enabled = true);

执行后可在订阅端查询 SELECT subname, received_lsn, latest_end_lsn, last_msg_send_time, last_msg_receipt_time FROM pg_stat_subscription;,若 latest_end_lsn 持续推进,说明增量流正常。发布端查询 SELECT slot_name, active, restart_lsn, confirmed_flush_lsn FROM pg_replication_slots WHERE slot_name = 'sub_app';,active = t 表示订阅端已连接,confirmed_flush_lsn 表示订阅端已确认消费到的位置。

如果初始拷贝数据量很大,copy_data = true 阶段会持续较久,此时订阅端表上会出现 COPY 相关活动;拷贝完成后才进入增量阶段。若希望先建订阅、稍后再启用,可把 enabled 设为 false,之后用 ALTER SUBSCRIPTION sub_app ENABLE; 启动。

常见参数取舍与排障方向

参数作用典型取舍
copy_data是否做初始快照拷贝新库同步用 true;已有全量数据用 false
create_slot是否自动在发布端建槽权限允许时用 true;受限环境手工建槽后设 false
enabled创建后是否立即启动需要先校验表结构时设 false
publish发布哪些操作类型只做增量追加可去掉 delete/truncate
publish_via_partition_root分区表以根表发布分区表同步建议 true

排障时按“订阅端状态 → 发布端复制槽 → 网络与权限”的顺序检查:订阅端 pg_stat_subscription 无记录,说明订阅未启用或连接失败;复制槽 active = f,说明订阅端未连上或已断开;restart_lsn 长时间不推进且 pg_wal 增长,说明订阅端消费停滞,需要检查订阅端是否有冲突、表结构是否匹配、网络是否可达。把发布定义、订阅连接参数和复制槽状态三者对齐,逻辑复制的问题通常就能快速定位。