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 是否推进。一次完整的建立过程与观察点
假设发布端 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 增长,说明订阅端消费停滞,需要检查订阅端是否有冲突、表结构是否匹配、网络是否可达。把发布定义、订阅连接参数和复制槽状态三者对齐,逻辑复制的问题通常就能快速定位。