publication / subscription 笔记
PostgreSQL 内置的逻辑复制,不需要装任何扩展。
搭建方式
数据库的配置
① 打开逻辑复制 (postgresql.conf,改完必须重启)
conf
wal_level = logical
max_replication_slots = 10 # 至少 ≥ 表数 + 几个余量
max_wal_senders = 10
② 不需要装扩展 ------ 内置的。
检查:
sql
SHOW wal_level; -- 必须 logical,replica 不行
SHOW max_replication_slots;
SHOW max_wal_senders;
角色权限配置
三种身份:
| 身份 | 要求 | 为什么 |
|---|---|---|
| 建 publication 的人 | 必须是这些表的 owner + 库级 CREATE | PG 硬性要求,给权限也不行 |
| 建 subscription 的人 | pg_create_subscription(PG16+)+ 库级 CREATE | |
| 连接账号(CONNECTION 里的 user) | REPLICATION | 连发布端拉 WAL |
建 publication 的人:
sql
-- 库级 CREATE(publication 是库级对象)
GRANT CREATE ON DATABASE db1 TO sale_admin;
-- ★ 13 张表必须都是它 owner,否则报 must be owner of table xxx
建 subscription 的人:
sql
-- 全局
ALTER ROLE sale_admin WITH REPLICATION;
GRANT pg_create_subscription TO sale_admin; -- PG 16+ 才有
| 权限 | 作用 |
|---|---|
| 表 owner | 把表加进 publication,必须是 owner |
| 库级 CREATE | 建 publication / subscription |
| REPLICATION | 连发布端拉 WAL |
| pg_create_subscription | 建订阅的独立资格(PG16+) |
⚠️ 内置复制最大的坑:非超级用户建订阅会被硬拦
ERROR: Non-superuser cannot connect if the server does not request a password
原因 :发布端的 pg_hba 如果是 trust(不问密码),PostgreSQL 不允许非超级用户走这条路 ------ 这个检查写在源码里,给什么权限都绕不过。
规避办法(按推荐顺序):
- 给连接账号设密码 + pg_hba 用 scram-sha-256(最干净)
- 建订阅时用超级用户
- 换 pglogical(它没这条检查)
源端需要执行的操作
sql
-- ① 建 publication(★ 显式列表,别用 FOR TABLES IN SCHEMA)
CREATE PUBLICATION sale_pub FOR TABLE
sale.merchants, sale.merchant_creators, sale.shops, sale.brands, sale.members,
sale.products, sale.shop_brands, sale.opportunities, sale.cooperations,
sale.samples, sale.opportunity_logs,
sale.fulfillment_lives, sale.fulfillment_videos;
-- ② 确认 13 张
SELECT count(*) FROM pg_publication_tables WHERE pubname = 'sale_pub';
FOR TABLES IN SCHEMA 需要超级用户,会报 must be superuser to create FOR TABLES IN SCHEMA publication。
同实例跨库要手工建槽(否则 CREATE SUBSCRIPTION 会自锁):
sql
SELECT pg_create_logical_replication_slot('sale_sub_slot', 'pgoutput');
同一个实例里自己订阅自己 ------ 标准写法会死锁:
CREATE SUBSCRIPTION 要建槽,而建槽要等订阅自己的事务结束 ------ 互相等。所以必须手工先把槽建好,建订阅时传 create_slot = false。
订阅端需要执行的操作
① 建表 ------ 同名同结构。只留 列名 + 列类型 + NOT NULL + 主键,去掉外键/触发器/默认值/CHECK。
模式名必须和源端一样 ------ 内置复制不能改表名、模式名。
② 建订阅
sql
CREATE SUBSCRIPTION sale_sub
CONNECTION 'host=127.0.0.1 port=5432 dbname=lite_sale_test
user=datahouse password=xxx sslmode=disable'
PUBLICATION sale_pub
WITH (create_slot = false, -- 槽已手工建好
slot_name = 'sale_sub_slot',
copy_data = true); -- 做一次初始全量
③ ★ 建完立刻确认它落在正确的库
sql
SELECT s.subname, d.datname AS 所在库, s.subenabled, s.subslotname
FROM pg_subscription s JOIN pg_database d ON d.oid = s.subdbid;
-- 期望:sale_sub | person_db_test | t | sale_sub_slot
pg_subscription 是共享目录 ------ 你在 db1 查也能看到 db2 的订阅。
判断订阅属于哪个库,必须 join subdbid。
④ 验证 ------ 两库逐张对行数,必须完全一致。
怎么取消同步
顺序不能反:订阅 → 发布 → 槽。
sql
-- 【订阅端】★ 先 DISABLE + 断开槽关联,再 DROP
ALTER SUBSCRIPTION sale_sub DISABLE;
ALTER SUBSCRIPTION sale_sub SET (slot_name = NONE);
DROP SUBSCRIPTION sale_sub;
为什么要先 DISABLE + SET slot_name = NONE :
DROP SUBSCRIPTION 会去连发布端删槽。如果连不上(网络断、发布端挂了),
整条命令会失败,订阅也删不掉。这两步先断开关联,DROP 就不需要联网了。
核查:
sql
SELECT count(*) FROM pg_subscription;
SELECT count(*) FROM pg_replication_slots; -- 应该没剩下
只停、留个底:
sql
ALTER SUBSCRIPTION sale_sub DISABLE; -- 恢复用 ENABLE
删发布端:
sql
DROP PUBLICATION IF EXISTS sale_pub;
遇到问题怎么排查
固定四步。
第 1 步 · 看订阅状态
sql
-- 【订阅端】注意 join subdbid(pg_subscription 是共享目录)
SELECT s.subname, d.datname AS 所在库, s.subenabled,
(st.pid IS NOT NULL) AS worker在跑
FROM pg_subscription s
JOIN pg_database d ON d.oid = s.subdbid
LEFT JOIN pg_stat_subscription st ON st.subid = s.oid;
第 2 步 · 看每张表的进度
sql
SELECT c.relname, sr.srsubstate
FROM pg_subscription_rel sr JOIN pg_class c ON c.oid = sr.relid
ORDER BY sr.srsubstate, c.relname;
-- i=初始化 d=正在拷 f=拷完 s=已同步 r=就绪 ✅
第 3 步 · 看日志第一条 ERROR
bash
docker logs 容器名 --since 10m | grep -iE "ERROR|FATAL" | head -5
# 云上:控制台 → 日志管理 → 错误日志
★ pg_subscription 里没有 last_error 这一列,报错只能翻日志。
第 4 步 · 对症处理
| 第一条 ERROR | 原因 | 怎么修 |
|---|---|---|
| Non-superuser cannot connect if the server does not request a password | 发布端 pg_hba 是 trust | 给账号设密码 + pg_hba 改 scram,或用超级用户建订阅 |
| must be owner of table xxx | 建 publication 的人不是表 owner | 换 owner 建,或用超级用户 |
| must be owner of publication xxx | 改 publication 的不是它的 owner | 同上 |
| must be superuser to create FOR TABLES IN SCHEMA publication | 用了 FOR TABLES IN SCHEMA | 改成显式列表 |
| duplicate key ... _pkey | 目标表非空,首同步撞主键 | TRUNCATE 目标表 → 删订阅重建 |
| 订阅 replicating、槽 active,但没数据 | 发布被清空了 | 见下 |
| ALTER SUBSCRIPTION ... does not exist | 连错库了 | join subdbid 确认 |
| CREATE SUBSCRIPTION 卡住不返回 | 同实例跨库自锁 | 手工建槽 + create_slot = false |
★ 最隐蔽的坑:发布被静默清空
DROP SCHEMA xxx CASCADE 或任何 DROP TABLE
→ PostgreSQL 会静默地把表从 publication 里移除 ------ 不报错、不提示
症状(全程不报错):订阅状态正常、槽 active、复制连接 streaming,但目标端永远是空的,pg_subscription_rel 是 0 行。
关键证据:
sql
SELECT count(*) FROM pg_publication_tables WHERE pubname='sale_pub'; -- 返回 0
修复两步,缺一不可:
sql
-- ① 【源】把表补回发布
ALTER PUBLICATION sale_pub ADD TABLE sale.merchants, sale.shops, ...;
-- ② 【目标】让订阅重新读发布端的表清单
ALTER SUBSCRIPTION sale_sub REFRESH PUBLICATION;
第 ② 步不能省。 apply worker 内部缓存了「空表清单」,
光补发布它不会自己重新读 ------ 必须 REFRESH 踢它一下。
其他需要注意的地方
失败会自己重试(和 pglogical 不一样)
订阅失败后每 5 秒重试一次 ,一直失败一直重试。修掉根因(删冲突行 / 补缺失列),它会自己恢复,不用手工拉。
pglogical 失败几次就放弃,必须手工 alter_subscription_enable 或重建订阅。
apply 以「订阅 owner」的身份跑
数据写进目标表时用的是订阅 owner 的身份,不是建表的人。
pglogical 不一样 ------ 它用的是「节点 DSN 里的用户」。
应用时会关掉触发器和外键
apply 以 session_replication_role = replica 运行:
- 外键被禁用 ------ 目标端有外键也从不生效
- 触发器被禁用 ------ 两库 updated_at 精确到微秒一致,说明 trg_*_updated_at 根本没跑
- 但 CHECK / NOT NULL / UNIQUE 照样生效 ------ 这些会拦住 apply
加表也必须你是 owner
ALTER PUBLICATION ... ADD TABLE 要求你是那张表的 owner。
pglogical 没这个要求 ------ 只要 SCHEMA 的 USAGE 就够。
DDL 完全不传
源端改表结构,目标端不会跟。要手工同步。
加列 会报错好发现;删列是静默的,最难发现。
不能改表名 / 模式名 / 列名
源端是什么,目标端就得是什么。想放 raw 层,在 sale 之上建视图。
目标端必须只读
本地改的数据会被同步静默覆盖,不报错。
孤儿同步槽
pg_19126_sync_18484_7688900967435198503 | active = f
首同步时每张表起一个同步 worker,每个建一个临时槽。worker 异常退出会留下孤儿槽,槽位耗尽后全线卡死。
sql
-- 先看
SELECT slot_name, active FROM pg_replication_slots WHERE slot_name LIKE 'pg\_%\_sync\_%';
-- 全删(只服务一次性拷贝,没有长期价值)
SELECT pg_drop_replication_slot(slot_name) FROM pg_replication_slots
WHERE slot_name LIKE 'pg\_%\_sync\_%' AND NOT active;
max_sync_workers_per_subscription 不是订阅参数
sql
-- ❌ 会报 unrecognized subscription parameter
CREATE SUBSCRIPTION ... WITH (max_sync_workers_per_subscription = 1);
它是服务器级 GUC(默认 2):
sql
ALTER SYSTEM SET max_sync_workers_per_subscription = 1;
SELECT pg_reload_conf(); -- sighup 级别,不用重启
参数建议
| 参数 | 建议 | 说明 |
|---|---|---|
| wal_level | logical | 启动参数,改了要重启 |
| max_replication_slots | ≥ 表数 + 余量 | 每张表首同步会开临时槽 |
| max_slot_wal_keep_size | 2~5 GB | 默认 -1 不限制,订阅挂了 WAL 会堆到磁盘满 |
| max_sync_workers_per_subscription | 2(默认) | 调成 1 更慢,但槽位不容易爆 |
槽跟着订阅生命周期
- CREATE SUBSCRIPTION 成功 → 立刻创建
- DROP SUBSCRIPTION → 立刻删除
和 pglogical 的关系
| 内置复制 | pglogical | |
|---|---|---|
| 装扩展 | 不用 | 要装(第三方) |
| 非超级用户建订阅 | ❌ 不行(源码硬拦) | ✅ 可以 |
| 加表要表 owner | ❌ 要 | ✅ 不要 |
| apply 身份 | 订阅 owner | 节点 DSN 里的用户 |
| 单表重同步 | ❌ 只能删订阅重建 | ✅ 有函数 |
| 序列同步 | ❌ | ✅ |
| DDL 传播 | ❌ | ✅ |
| 失败自动重试 | ✅ 每 5 秒 | ❌ 放弃 |
一句话记忆
| 顺序 | 建表 → 建 publication(源)→ 手工建槽 → 建 subscription(目标) |
| 三个身份 | publication 建者(表 owner )· subscription 建者(pg_create_subscription)· 连接账号(REPLICATION) |
| 最大的坑 | 非超级用户 + pg_hba trust = 建不了订阅,绕不过 |
| 最隐蔽的故障 | 发布被静默清空 ------ 订阅正常,就是没数据 |
| 失败会自愈 | 每 5 秒重试,修好根因自动恢复 |
| 取消同步 | DISABLE → SET slot_name=NONE → DROP SUBSCRIPTION |