PostgreSQL 16 逻辑复制原理、落地与故障排查
当你只想把 A 库中的几张业务表持续同步到 B 库,而不是搭建一套完整的物理主从时,PostgreSQL 原生逻辑复制通常是最直接的方案。
本文不是一份“复制粘贴命令清单”,而是从问题背景、工作原理、设计边界、实施过程、故障排查和后续运维六个层面,完整讲清楚 PostgreSQL 表级逻辑复制。
文中所有 IP、库名、schema、表名、账号和密码均为脱敏示例。
写在前面:很多时候,我们并不需要一个“从库”
数据库同步需求看起来都很像:
A 库有数据
↓
把数据同步到 B 库但“同步到 B 库”背后可能是完全不同的目标:
- 为生产库建设只读灾备;
- 为查询业务提供只读副本;
- 将部分业务数据同步到报表库;
- 将某个 schema 的表同步到测试环境;
- 在数据库迁移期间做增量追平;
- 将订单表同步给另一个独立系统;
- 为数据分析平台提供准实时数据源。
如果目标是高可用和整库灾备,通常应该考虑物理流复制。
但如果你的真实需求是:
只同步几张表,B 库仍然是一套独立数据库,并且 B 库中还有自己的其他表和业务。
那么物理主从就显得过重了。
这正是 PostgreSQL 逻辑复制擅长解决的问题。
先给结论
PostgreSQL 原生逻辑复制的核心模型只有一句话:
A 库通过 Publication 声明“我要发布哪些表的数据变化”,
B 库通过 Subscription 声明“我要订阅哪个发布”。数据链路可以简化为:
业务写入 A 库
↓
A 库生成 WAL
↓
逻辑解码把 WAL 转换成行级变化
↓
Publication 过滤需要发布的表和操作
↓
Replication Slot 记录 B 库消费位置
↓
B 库 Subscription 接收变化
↓
Apply Worker 将变化应用到同名目标表不过,真正让逻辑复制稳定运行的并不是两条 CREATE 语句,而是下面这些条件同时成立:
- A 库
wal_level=logical; - A/B 网络和认证正常;
- 复制账号具备正确权限;
- B 库提前存在兼容的表结构;
- 发布表具备主键或合适的 Replica Identity;
- B 库目标表不能被业务随意写入;
- 复制槽和 WAL 保留量被持续监控;
- 表结构变更遵守固定顺序;
- 订阅错误、超时和表同步卡顿有明确排查方法。
本文会围绕这些问题展开。
一、本文使用的环境
A 库:发布端
IP:192.168.1.1
端口:5432
数据库:test_db
schema:test_sync
Publication:test_sync_pub
复制用户:repl_testB 库:订阅端
IP:192.168.1.2
端口:5432
数据库:test_db
schema:test_sync
Subscription:test_sync_sub示例表
test_customer
test_order
test_order_item
test_task
test_metric表名统一使用 test_xxx,而不是 test-xxx。
原因是 PostgreSQL 标识符一旦包含连字符,就必须始终使用双引号:
SELECT *
FROM "test-order";这会增加 SQL 编写和运维成本,因此示例使用更常见的下划线命名。
二、逻辑复制到底“逻辑”在哪里
理解逻辑复制之前,先要分清两个概念:
- 物理复制;
- 逻辑复制。
2.1 物理复制:复制的是数据库底层变化
物理流复制传递的是 WAL 中的物理变化。
可以粗略理解为:
A 库某个数据页发生变化
↓
WAL 记录这个底层变化
↓
B 库按相同物理方式重放它关注的是数据库集群整体一致性,而不是某张业务表。
因此物理复制天然适合:
- 高可用;
- 整库灾备;
- 只读副本;
- 主备切换。
但它的粒度比较粗。
通常不能说:
我只想复制 test_order,
但不复制 test_customer。2.2 逻辑复制:复制的是行级数据变化
逻辑复制同样建立在 WAL 之上,但它不是把 WAL 当作数据库页修改直接回放,而是通过逻辑解码,将其转换为类似下面的逻辑变化:
表:test_sync.test_order
操作:INSERT
主键:10001
字段:
order_no = TEST-10001
amount = 128.50B 库接收到的不是原始 SQL 文本,也不是底层数据页,而是经过协议编码的行级变化。
因此它可以做到:
- 只发布指定表;
- 只发布某些 DML 操作;
- 一个 Publication 被多个 Subscriber 订阅;
- B 库保留自己的其他对象;
- 在兼容条件下用于跨版本迁移;
- 让目标库继续承担独立查询和分析工作。
2.3 物理复制和逻辑复制怎么选
| 场景 | 更适合的方案 |
|---|---|
| PostgreSQL 高可用 | 物理流复制 |
| 整库只读副本 | 物理流复制 |
| 整库灾备 | 物理流复制 |
| 只同步部分表 | 逻辑复制 |
| 将业务表分发给多个系统 | 逻辑复制 |
| 数据库迁移期间增量追平 | 逻辑复制 |
| 为报表库提供部分实时数据 | 逻辑复制 |
| B 库需要保留独立数据 | 逻辑复制 |
没有绝对更好的复制方式,只有是否适合当前目标。
三、从一条 INSERT 看懂完整复制链路
假设业务在 A 库执行:
INSERT INTO test_sync.test_order(
id,
order_no,
amount,
created_at
)
VALUES (
10001,
'TEST-10001',
128.50,
now()
);这条数据如何到达 B 库?
第一步:A 库写入 WAL
PostgreSQL 遵循 Write-Ahead Logging,也就是预写日志。
在真正修改数据文件之前,数据库先记录 WAL。
WAL 为 PostgreSQL 提供:
- 崩溃恢复;
- 物理复制;
- 时间点恢复;
- 逻辑解码;
- 逻辑复制。
第二步:逻辑解码读取 WAL
当:
wal_level = logicalPostgreSQL 会在 WAL 中保留足够的信息,使输出插件能够将 WAL 解码为逻辑变化。
原生逻辑复制通常使用内置输出插件:
pgoutput第三步:Publication 判断是否发布
A 库中创建:
CREATE PUBLICATION test_sync_pub
FOR TABLES IN SCHEMA test_sync
WITH (
publish = 'insert, update, delete, truncate'
);Publication 相当于一份发布规则:
只关注 test_sync schema 中的表;
发布 INSERT、UPDATE、DELETE、TRUNCATE。如果业务修改的是另一个未发布的 schema,这条变化不会进入该 Publication。
第四步:Replication Slot 记录消费进度
Subscription 通常会在 A 库创建一个逻辑复制槽。
例如:
test_sync_sub复制槽的作用不是“存放完整数据”,而是记录消费者已经确认到哪个 WAL 位置。
例如:
A 库当前 WAL:0/A000000
B 库已确认: 0/8000000这意味着:
0/8000000 之后的 WAL 暂时不能被清理,
因为 B 库还没有消费完成。第五步:B 库 Apply Worker 应用变化
B 库 Subscription 接收到变化后,由 Apply Worker 将其写入:
test_sync.test_order至此,一次 INSERT 完成了从 A 到 B 的传递。
完整过程:
INSERT
↓
WAL
↓
Logical Decoding
↓
Publication
↓
Replication Slot
↓
Subscription
↓
Apply Worker
↓
B 库目标表四、逻辑复制中的五个关键角色
很多逻辑复制故障,本质上是没有分清不同对象的职责。
4.1 Publisher
Publisher 是发布端数据库。
本文中:
192.168.1.1它负责:
- 接收业务写入;
- 生成 WAL;
- 进行逻辑解码;
- 提供 Publication;
- 维护复制槽;
- 向 Subscriber 发送变化。
4.2 Publication
Publication 是发布规则。
它回答两个问题:
发布哪些表?
发布哪些操作?例如:
CREATE PUBLICATION test_sync_pub
FOR TABLES IN SCHEMA test_sync
WITH (
publish = 'insert, update, delete, truncate'
);Publication 本身不保存数据,也不主动连接 B 库。
它只是描述“哪些变化允许被订阅”。
4.3 Subscriber
Subscriber 是订阅端数据库。
本文中:
192.168.1.2它不是物理意义上的只读备库,而是一套独立 PostgreSQL 实例。
它可以:
- 拥有自己的用户;
- 拥有自己的配置;
- 拥有其他数据库;
- 拥有其他 schema;
- 承载查询、报表或分析业务。
但被复制的目标表不应该被其他业务随意写入。
4.4 Subscription
Subscription 定义:
- A 库地址;
- A 库端口;
- 数据库名称;
- 复制用户;
- 复制密码;
- 要订阅的 Publication;
- 是否复制存量;
- 是否创建复制槽;
- 是否立即启用;
- 出错后是否自动停用。
4.5 Replication Slot
复制槽用于保证 Subscriber 尚未消费的 WAL 不会过早删除。
它提供可靠续传能力,但也带来磁盘风险。
这是一把双刃剑:
没有复制槽:
B 库断开后可能丢失所需 WAL。
有复制槽但不监控:
B 库长期离线可能导致 A 库 WAL 撑满磁盘。因此,逻辑复制上线后必须把复制槽纳入监控。
五、初始同步和实时同步不是一回事
创建 Subscription 时通常会设置:
copy_data = true这会触发两个阶段。
5.1 第一阶段:复制已有数据
假设 A 库 test_order 已有 500 万行。
Subscription 创建后,PostgreSQL 会对目标表执行初始同步。
可以理解为:
A 库现有数据
↓
COPY
↓
B 库目标表这个阶段会启动表同步 Worker。
如果配置:
max_sync_workers_per_subscription = 2表示一个 Subscription 同时最多有两张表进行初始 COPY。
其余表会排队。
所以看到:
2 张表处于 d
其他表处于 i是正常现象,不代表遗漏。
5.2 第二阶段:追赶增量
在初始 COPY 期间,A 库可能仍在持续写入。
PostgreSQL必须保证:
存量数据复制完成
+
COPY 期间发生的增量变化都被正确应用。
因此表状态不会简单地从 d 直接消失,而会经历协调过程。
5.3 表状态 i、d、f、s、r 分别是什么
在 B 库查询:
SELECT
r.srrelid::regclass AS table_name,
r.srsubstate
FROM pg_subscription_rel r
JOIN pg_subscription s
ON s.oid = r.srsubid
WHERE s.subname = 'test_sync_sub'
ORDER BY table_name;常见状态:
| 状态 | 含义 |
|---|---|
i |
初始化或等待调度 |
d |
正在复制存量数据 |
f |
存量 COPY 已完成 |
s |
已同步,等待与主 Apply Worker 协调 |
r |
正常实时复制 |
真正的最终状态只有:
r如果一共有 5 张表,最终应该看到:
r | 55.4 为什么初始化完成后只剩一个 Worker
初始化期间可能看到:
主 Apply Worker
test_sync.test_order
test_sync.test_order_item其中:
- 主 Apply Worker:负责持续应用增量变化;
- 表同步 Worker:负责某张表的初始 COPY。
当全部表进入 r 后,表同步 Worker 会退出,通常只剩主 Apply Worker。
如果主 Apply Worker 显示:
state = idle
wait_event = LogicalApplyMain这通常是正常空闲状态,表示当前没有新的 WAL 需要处理,不是卡住。
六、逻辑复制的边界:它不会帮你做什么
很多第一次使用逻辑复制的人,会下意识认为:
既然数据能同步,表结构应该也会同步。实际上不会。
6.1 不同步 DDL
逻辑复制不会自动同步:
CREATE SCHEMACREATE TABLEALTER TABLEDROP TABLE- 新增字段
- 删除字段
- 修改字段类型
- 索引
- 主键
- 唯一约束
- 外键
- 触发器定义
这意味着:
B 库必须提前存在同 schema、同表名、兼容字段结构的目标表。
6.2 不同步序列当前值
假设 A 库使用:
id bigserial表中的 id 数据会同步,但序列当前值不会自动跟随。
所以 B 库不适合作为可随时切换写入的节点。
如果在 B 库直接插入数据,可能出现:
- 序列值落后;
- 主键冲突;
- 后续复制中断。
6.3 不同步视图、函数和存储过程
逻辑复制关注的是表数据变化。
不会自动同步:
- 视图;
- 物化视图;
- 函数;
- 存储过程;
- 用户;
- 权限。
6.4 不同步 UNLOGGED 表
UNLOGGED 表不写完整 WAL,因此无法参与正常逻辑复制。
检查:
SELECT
n.nspname,
c.relname,
c.relpersistence
FROM pg_class c
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE n.nspname = 'test_sync'
AND c.relkind IN ('r', 'p');常见值:
p = 持久化表
u = UNLOGGED 表
t = 临时表七、真正开始之前,先做六个设计决策
很多文章直接从 ALTER SYSTEM 开始,但生产实施更重要的是先做设计。
7.1 决定同步范围
需要明确:
- 同步整个 schema;
- 还是显式指定几张表;
- 未来新增表是否自动纳入;
- 是否发布
TRUNCATE; - 是否需要全部字段;
- 是否需要过滤行。
本文采用:
FOR TABLES IN SCHEMA test_sync优点:
- 当前表自动纳入;
- 未来该 schema 新建表也会进入 Publication 范围。
代价:
- 很难在同一 schema 中单独排除某张普通表;
- 新增表后,B 库必须提前准备结构;
- 新表权限和 Subscription 刷新需要规范化处理。
7.2 决定 B 库是否允许写入
最安全的策略是:
B 库复制表只读。如果允许业务写入,就必须面对:
- 主键冲突;
- 更新覆盖;
- 删除冲突;
- 数据分叉;
- Subscription 停止。
原生单向逻辑复制不是双主方案。
7.3 检查主键和 Replica Identity
对于 UPDATE 和 DELETE,B 库必须知道应该修改哪一行。
PostgreSQL 默认使用主键作为 Replica Identity。
检查:
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
EXISTS (
SELECT 1
FROM pg_index i
WHERE i.indrelid = c.oid
AND i.indisprimary
) AS has_primary_key,
c.relreplident
FROM pg_class c
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE n.nspname = 'test_sync'
AND c.relkind IN ('r', 'p')
ORDER BY c.relname;没有主键的表应优先补主键。
实在无法增加主键时,可以考虑:
ALTER TABLE test_sync.test_xxx
REPLICA IDENTITY FULL;但它会增加 WAL、网络和匹配成本。
7.4 决定是否复制存量数据
首次搭建通常使用:
copy_data = true适合:
- B 库目标表为空;
- 希望 PostgreSQL 自动复制历史数据;
- 不想自己处理存量导入。
如果 B 库已有一份确认一致的数据,可以使用:
copy_data = false但这要求你自己保证 A/B 存量数据一致。
否则逻辑复制只会同步创建 Subscription 之后的新变化,历史差异会永久存在。
7.5 决定复制槽 WAL 保留策略
如果:
max_slot_wal_keep_size = -1表示复制槽可无限保留 WAL。
好处:
- B 库离线较久后仍有机会续传。
风险:
- A 库磁盘可能被 WAL 占满。
生产环境必须在“可靠续传”和“磁盘安全”之间做取舍。
7.6 决定结构变更顺序
最重要的变更规范:
新增字段:B 先,A 后。原因很简单:
如果 A 库先新增字段并开始写入,B 库没有该字段,Subscription 会报:
missing replicated columns八、完整实施:A 库发布端
这一部分不仅给命令,还会解释每一步为什么要做、如何验证以及最常见的错误。
8.1 第一步:确认连接的是正确数据库
Publication 是数据库级对象。
如果 test_sync 位于 test_db,但当前连接的是默认 postgres 数据库,执行创建 Publication 会报:
ERROR: schema "test_sync" does not exist先检查:
SELECT
current_database(),
current_user;切换:
\c test_db验证 schema:
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = 'test_sync';8.2 第二步:检查表类型、主键和持久化属性
SELECT
n.nspname AS schema_name,
c.relname AS table_name,
CASE c.relkind
WHEN 'r' THEN '普通表'
WHEN 'p' THEN '分区表'
END AS table_type,
CASE c.relpersistence
WHEN 'p' THEN '持久化表'
WHEN 'u' THEN 'UNLOGGED表'
WHEN 't' THEN '临时表'
END AS persistence,
EXISTS (
SELECT 1
FROM pg_index i
WHERE i.indrelid = c.oid
AND i.indisprimary
) AS has_primary_key,
c.relreplident
FROM pg_class c
JOIN pg_namespace n
ON n.oid = c.relnamespace
WHERE n.nspname = 'test_sync'
AND c.relkind IN ('r', 'p')
ORDER BY c.relname;检查目标:
表类型正确
持久化属性为 p
需要 UPDATE/DELETE 的表具备主键
表数量符合预期8.3 第三步:开启逻辑 WAL
检查当前参数:
SHOW wal_level;如果是:
replica设置:
ALTER SYSTEM SET wal_level = 'logical';查看参数来源:
SELECT
name,
setting,
source,
sourcefile,
pending_restart
FROM pg_settings
WHERE name = 'wal_level';这里经常出现一个误区:
SELECT pg_reload_conf();并不能让 wal_level 立即生效。
因为 wal_level 是启动级参数,必须重启 PostgreSQL。
sudo systemctl restart postgresql重启后:
SHOW wal_level;预期:
logical8.4 第四步:检查复制能力参数
SHOW max_replication_slots;
SHOW max_wal_senders;
SHOW max_slot_wal_keep_size;
SHOW wal_sender_timeout;这些参数分别控制:
- 可创建多少复制槽;
- 可启动多少 WAL Sender;
- 单个槽最多保留多少 WAL;
- A 库等待接收端响应多久。
注意:
max_replication_slots = 10表示最多有 10 个复制槽,不是只能同步 10 张表。
8.5 第五步:创建最小权限复制账号
CREATE ROLE repl_test
WITH
LOGIN
REPLICATION
PASSWORD '<REPLICATION_PASSWORD>';连接权限:
GRANT CONNECT
ON DATABASE test_db
TO repl_test;schema 权限:
GRANT USAGE
ON SCHEMA test_sync
TO repl_test;现有表读取权限:
GRANT SELECT
ON ALL TABLES IN SCHEMA test_sync
TO repl_test;未来新表默认权限:
ALTER DEFAULT PRIVILEGES
FOR ROLE test_owner
IN SCHEMA test_sync
GRANT SELECT ON TABLES TO repl_test;这里有一个容易忽略的细节:
ALTER DEFAULT PRIVILEGES只影响指定对象所有者未来创建的对象。
如果业务表是由 test_owner 创建,就必须写:
FOR ROLE test_owner否则未来新增表可能因为复制账号没有 SELECT 权限而同步失败。
8.6 第六步:配置 pg_hba.conf
查询文件:
SHOW hba_file;
SHOW listen_addresses;增加:
host test_db repl_test 192.168.1.2/32 scram-sha-256重新加载:
SELECT pg_reload_conf();还需要确认:
- A 库监听地址允许远程连接;
- A 库主机防火墙允许 B 库访问;
- 网络 ACL 或安全组允许 TCP 5432;
- B 库实际出口地址就是
192.168.1.2。
不要为了省事写:
0.0.0.0/08.7 第七步:创建 Publication
CREATE PUBLICATION test_sync_pub
FOR TABLES IN SCHEMA test_sync
WITH (
publish = 'insert, update, delete, truncate'
);检查 Publication:
SELECT
pubname,
pubinsert,
pubupdate,
pubdelete,
pubtruncate
FROM pg_publication
WHERE pubname = 'test_sync_pub';检查表清单:
SELECT
pubname,
schemaname,
tablename
FROM pg_publication_tables
WHERE pubname = 'test_sync_pub'
ORDER BY schemaname, tablename;不要只看:
CREATE PUBLICATION必须核对表数量和名称。
九、B 库连接 A 库:先验证链路,再创建订阅
创建 Subscription 之前,应先从 B 服务器直接测试 A 库。
psql \
-h 192.168.1.1 \
-p 5432 \
-U repl_test \
-d test_db连接后:
SELECT
current_database(),
current_user;
SELECT count(*)
FROM test_sync.test_order;这一小步可以提前发现:
- 防火墙不通;
- 端口不通;
pg_hba.conf不匹配;- 密码错误;
- 数据库无 CONNECT 权限;
- schema 无 USAGE 权限;
- 表无 SELECT 权限。
不要等到 CREATE SUBSCRIPTION 失败后再排查基础网络。
十、完整实施:B 库订阅端
10.1 第一步:检查 Worker 参数
SHOW max_replication_slots;
SHOW max_logical_replication_workers;
SHOW max_worker_processes;
SHOW max_sync_workers_per_subscription;
SHOW wal_receiver_timeout;最容易被误解的是:
max_sync_workers_per_subscription它控制的是初始同步阶段,同时有多少张表进行 COPY。
例如:
max_sync_workers_per_subscription = 2即使有 100 张表,也只是每次并发两张,其余表排队。
10.2 第二步:准备同名表结构
逻辑复制不会自动创建目标表。
因此需要从 A 库导出 schema-only。
推荐使用自定义格式:
export PGPASSWORD='<A_DATABASE_PASSWORD>'
pg_dump \
-h 192.168.1.1 \
-p 5432 \
-U postgres \
-d test_db \
--format=custom \
--schema-only \
--schema=test_sync \
--no-owner \
--no-privileges \
--file=/tmp/test_sync_schema.dump为什么推荐 .dump 而不是普通 .sql?
因为普通 SQL 文本很容易出现:
- 被
>>追加多次; - 文件中间被截断;
- 手工编辑损坏;
- 多个 dump 被拼接;
- 导入错误后继续执行。
自定义格式可以先验证:
pg_restore \
--list \
/tmp/test_sync_schema.dump \
>/tmp/test_sync_schema.list
echo $?退出码应为:
010.3 第三步:检查 schema 外部依赖
如果发布表的外键指向另一个 schema:
test_sync.test_order.created_by
→ public.test_user.id仅导出 test_sync 时,不会自动导出 public.test_user。
恢复前检查:
SELECT to_regclass('public.test_user');否则恢复可能在创建外键时失败。
10.4 第四步:清理旧结构
首次部署前,目标表应为空。
如果曾经失败并需要重建,应按照下面顺序清理:
停业务写入
↓
停用或删除旧 Subscription
↓
确认复制 Worker 退出
↓
终止长事务和阻塞连接
↓
删除旧 schema
↓
恢复最新结构删除:
DROP SCHEMA IF EXISTS test_sync CASCADE;为避免无限等待:
SET lock_timeout = '15s';10.5 第五步:原子化恢复
export PGPASSWORD='<B_DATABASE_PASSWORD>'
pg_restore \
-h 192.168.1.2 \
-p 5432 \
-U postgres \
--role=test_owner \
-d test_db \
--no-owner \
--no-privileges \
--exit-on-error \
--single-transaction \
--verbose \
/tmp/test_sync_schema.dump关键参数:
--role=test_owner让恢复后的对象归业务角色。
--exit-on-error遇到错误立即退出。
--single-transaction整个恢复失败时统一回滚,避免留下半套结构。
10.6 第六步:验证结构和空表
表清单:
SELECT
tablename,
tableowner
FROM pg_tables
WHERE schemaname = 'test_sync'
ORDER BY tablename;字段清单:
SELECT
table_name,
ordinal_position,
column_name,
data_type,
udt_name,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_schema = 'test_sync'
ORDER BY table_name, ordinal_position;检查每张表行数:
SELECT format(
'SELECT %L AS table_name, count(*) AS row_count FROM %I.%I;',
schemaname || '.' || tablename,
schemaname,
tablename
)
FROM pg_tables
WHERE schemaname = 'test_sync'
ORDER BY tablename
\gexec首次使用 copy_data=true 时,目标表应为空。
10.7 第七步:创建 Subscription
CREATE SUBSCRIPTION test_sync_sub
CONNECTION '
host=192.168.1.1
port=5432
dbname=test_db
user=repl_test
password=<REPLICATION_PASSWORD>
application_name=test_sync_sub
connect_timeout=10
'
PUBLICATION test_sync_pub
WITH (
copy_data = true,
create_slot = true,
enabled = true,
disable_on_error = true
);参数解释:
copy_data=true:先复制历史数据;create_slot=true:在 A 库创建逻辑复制槽;enabled=true:创建后立即启动;disable_on_error=true:出现复制错误时自动停用;application_name:便于 A 库从pg_stat_replication识别连接。
十一、如何判断“真的同步完成了”
看到 Subscription 创建成功,不代表所有表已经同步完成。
11.1 检查 Subscription 是否启用
SELECT
subname,
subenabled,
subslotname,
subdisableonerr,
subpublications
FROM pg_subscription
WHERE subname = 'test_sync_sub';目标:
subenabled = t11.2 查看所有表状态
SELECT
r.srrelid::regclass AS table_name,
r.srsubstate,
CASE r.srsubstate
WHEN 'i' THEN '初始化排队'
WHEN 'd' THEN '复制存量数据'
WHEN 'f' THEN '存量复制完成'
WHEN 's' THEN '已同步,等待切换'
WHEN 'r' THEN '正常实时复制'
ELSE '未知状态'
END AS sync_state
FROM pg_subscription_rel r
JOIN pg_subscription s
ON s.oid = r.srsubid
WHERE s.subname = 'test_sync_sub'
ORDER BY table_name;汇总:
SELECT
r.srsubstate,
count(*) AS table_count
FROM pg_subscription_rel r
JOIN pg_subscription s
ON s.oid = r.srsubid
WHERE s.subname = 'test_sync_sub'
GROUP BY r.srsubstate
ORDER BY r.srsubstate;最终标准:
r | 5假设一共 5 张表。
11.3 查看 Worker
SELECT
ps.pid,
CASE
WHEN ps.relid IS NULL THEN '主 Apply Worker'
ELSE ps.relid::regclass::text
END AS worker,
a.state,
a.wait_event_type,
a.wait_event,
ps.latest_end_time
FROM pg_stat_subscription ps
LEFT JOIN pg_stat_activity a
ON a.pid = ps.pid
WHERE ps.subname = 'test_sync_sub'
ORDER BY ps.relid NULLS FIRST;全部完成后通常只剩主 Apply Worker。
11.4 查看 COPY 是否推进
SELECT
pid,
relid::regclass AS table_name,
command,
type,
pg_size_pretty(bytes_processed) AS processed,
tuples_processed
FROM pg_stat_progress_copy;持续观察:
SELECT
clock_timestamp(),
pid,
relid::regclass AS table_name,
bytes_processed,
tuples_processed
FROM pg_stat_progress_copy
\watch 10如果数值持续增长,说明正在正常 COPY。
十二、一次完整验证:INSERT、UPDATE、DELETE
只看系统视图还不够,最终必须做业务级验证。
12.1 INSERT
A 库:
INSERT INTO test_sync.test_order(
id,
order_no,
amount,
created_at
)
VALUES (
900001,
'TEST-ORDER-900001',
100.00,
now()
);B 库:
SELECT *
FROM test_sync.test_order
WHERE id = 900001;12.2 UPDATE
A 库:
UPDATE test_sync.test_order
SET amount = 200.00
WHERE id = 900001;B 库:
SELECT
id,
order_no,
amount
FROM test_sync.test_order
WHERE id = 900001;12.3 DELETE
A 库:
DELETE FROM test_sync.test_order
WHERE id = 900001;B 库:
SELECT *
FROM test_sync.test_order
WHERE id = 900001;预期:
0 rows十三、真实实施中最容易踩的八个坑
下面这些问题并不是理论推演,而是逻辑复制实施中非常典型的故障。
坑一:在错误数据库创建 Publication
现象:
ERROR: schema "test_sync" does not exist先检查:
SELECT current_database();Publication 是数据库级对象,必须连接包含目标 schema 的数据库。
坑二:设置了 wal_level,但没有重启
执行:
ALTER SYSTEM SET wal_level = 'logical';之后只做:
SELECT pg_reload_conf();仍然不会生效。
检查:
SELECT
setting,
pending_restart
FROM pg_settings
WHERE name = 'wal_level';只要需要重启,就必须安排数据库重启。
坑三:普通 SQL dump 被追加或截断
典型表现:
一个文件里出现多个 dump complete
SQL 从半行开始
约束定义被截断解决办法不是手工修几行,而是重新导出自定义格式:
pg_dump --format=custom并使用:
pg_restore --list验证归档完整性。
坑四:B 库缺少 A 库新字段
错误:
logical replication target relation
is missing replicated columns根因:
逻辑复制不同步 DDL。修复:
- 在 A 库确认准确字段定义;
- 在 B 库补齐;
- 再恢复 Subscription。
坑五:B 库已有数据导致主键冲突
错误:
duplicate key value violates unique constraint常见原因:
- B 库目标表未清空;
- 重建 Subscription 时再次
copy_data=true; - B 库被其他业务写入。
最稳妥方式:
停止写入
→ 清理目标表
→ 重建结构
→ 重新初始同步坑六:Subscription 自动变成 f
查询:
SELECT
subenabled,
subdisableonerr
FROM pg_subscription
WHERE subname = 'test_sync_sub';如果:
subenabled = f说明订阅已停用。
若:
subdisableonerr = t则一次复制错误就可能自动禁用订阅。
不要直接反复:
ALTER SUBSCRIPTION test_sync_sub ENABLE;应该先查看最新错误日志和统计。
坑七:初始同步最后一张表长期停在 d
先看进度:
SELECT
pid,
relid::regclass AS table_name,
bytes_processed,
tuples_processed
FROM pg_stat_progress_copy;隔一段时间再次查询。
如果长时间完全不增长,再检查 Worker:
SELECT
ps.pid,
ps.relid::regclass AS table_name,
a.wait_event_type,
a.wait_event
FROM pg_stat_subscription ps
LEFT JOIN pg_stat_activity a
ON a.pid = ps.pid
WHERE ps.subname = 'test_sync_sub';确认某个表同步 Worker 已异常卡死后,可以只终止该 Worker:
SELECT pg_terminate_backend(<TABLE_SYNC_WORKER_PID>);不要随意终止主 Apply Worker。
坑八:复制 Worker 因超时退出
日志:
terminating logical replication worker due to timeout参数位置必须分清:
A 库:wal_sender_timeout
B 库:wal_receiver_timeout跨网络链路可以适当增大:
-- A 库
ALTER SYSTEM SET wal_sender_timeout = '15min';
SELECT pg_reload_conf();
-- B 库
ALTER SYSTEM SET wal_receiver_timeout = '15min';
SELECT pg_reload_conf();如果仍然超时,不应无限增大参数,而应进一步检查:
- 网络丢包;
- NAT 会话回收;
- 防火墙连接超时;
- TCP keepalive;
- A/B 数据库 I/O;
- 容器网络;
- 大表 COPY;
- 连接是否持续收到 keepalive。
十四、表结构变更应该怎么做
逻辑复制长期稳定运行的关键,不是第一次搭建,而是后续 DDL 管理。
14.1 新增字段:B 先,A 后
-- B 库先执行
ALTER TABLE test_sync.test_order
ADD COLUMN test_remark varchar(500);
-- A 库后执行
ALTER TABLE test_sync.test_order
ADD COLUMN test_remark varchar(500);然后再发布使用该字段的业务代码。
顺序:
B 库结构兼容
↓
A 库新增字段
↓
业务开始写新字段14.2 删除字段:先停止业务使用
推荐:
业务停止读写字段
↓
确认 A 库不再使用
↓
A 库删除字段
↓
验证复制正常
↓
B 库删除字段不要先在 B 库删除仍由 A 库发送的字段。
14.3 新增表
使用:
FOR TABLES IN SCHEMA test_sync时,A 库以后在该 schema 中新建的普通表会自动进入 Publication 范围。
但 B 库不会自动建表。
正确流程:
B 库先创建同名表
↓
设置正确 owner
↓
A 库创建表
↓
给复制用户 SELECT
↓
B 库 REFRESH PUBLICATION
↓
观察新表进入 r刷新:
ALTER SUBSCRIPTION test_sync_sub
REFRESH PUBLICATION
WITH (
copy_data = true
);14.4 修改字段类型
修改字段类型必须确保两边兼容。
稳妥策略:
- B 库先扩展为兼容类型;
- A 库再修改;
- 验证复制;
- 最后上线新业务代码。
复杂类型转换应进入维护窗口,不应在高峰期直接操作。
十五、如何建立真正有效的监控
逻辑复制监控可以分成三个层面。
15.1 控制面:Subscription 是否正常
B 库:
SELECT
s.subname,
s.subenabled,
s.subdisableonerr,
st.apply_error_count,
st.sync_error_count
FROM pg_subscription s
LEFT JOIN pg_stat_subscription_stats st
ON st.subid = s.oid
WHERE s.subname = 'test_sync_sub';应该告警:
subenabled=false;apply_error_count增长;sync_error_count增长;- 主 Apply Worker 不存在。
15.2 数据面:Worker 是否推进
SELECT
ps.pid,
ps.relid::regclass AS syncing_table,
a.state,
a.wait_event_type,
a.wait_event,
ps.received_lsn,
ps.latest_end_lsn,
ps.latest_end_time
FROM pg_stat_subscription ps
LEFT JOIN pg_stat_activity a
ON a.pid = ps.pid
WHERE ps.subname = 'test_sync_sub';应该关注:
- Worker 是否存在;
latest_end_time是否长时间不更新;- 表同步进度是否停滞;
- 正常阶段是否出现非
r表。
15.3 存储面:复制槽保留多少 WAL
A 库:
SELECT
slot_name,
active,
wal_status,
pg_size_pretty(
pg_wal_lsn_diff(
pg_current_wal_lsn(),
restart_lsn
)
) AS retained_wal
FROM pg_replication_slots
WHERE slot_name = 'test_sync_sub';必须告警:
- 复制槽长时间 inactive;
retained_wal持续增长;wal_status异常;- A 库磁盘使用率过高;
pg_wal目录异常增长。
十六、日常运维命令
暂停
ALTER SUBSCRIPTION test_sync_sub DISABLE;注意:Worker 可能不会瞬间退出。
SELECT
pid,
relid::regclass AS syncing_table
FROM pg_stat_subscription
WHERE subname = 'test_sync_sub';恢复
ALTER SUBSCRIPTION test_sync_sub ENABLE;删除
DROP SUBSCRIPTION test_sync_sub;正常情况下会尝试删除 A 库对应复制槽。
A 库不可达时删除
ALTER SUBSCRIPTION test_sync_sub DISABLE;
ALTER SUBSCRIPTION test_sync_sub
SET (
slot_name = NONE
);
DROP SUBSCRIPTION test_sync_sub;之后在 A 库手工检查遗留槽。
修改复制密码
ALTER SUBSCRIPTION test_sync_sub
CONNECTION '
host=192.168.1.1
port=5432
dbname=test_db
user=repl_test
password=<NEW_REPLICATION_PASSWORD>
application_name=test_sync_sub
connect_timeout=10
';十七、FAQ:常见认知误区
1. max_replication_slots=10 是否只能同步 10 张表
不是。
它限制的是复制槽数量,不是表数量。
一个 Subscription 可以包含很多张表。
2. B 库可以继续写同步表吗
技术上可能允许,但不建议。
单向逻辑复制没有自动冲突解决能力。
3. B 库表字段顺序必须完全一致吗
目标表需要具备兼容列。
最稳妥的方式是直接从 A 库导出 schema-only 恢复,而不是人工重建。
4. 逻辑复制能代替高可用吗
不能直接等同。
它适合表级数据分发和迁移,不等于完整主备切换体系。
5. Subscription 创建成功是否代表完成
不代表。
必须确认所有表:
srsubstate = r6. B 库断开后会不会丢数据
只要复制槽仍可用且所需 WAL 没有丢失,恢复连接后可以继续追赶。
但 B 库长时间离线会导致 A 库 WAL 堆积。
7. 为什么 B 库没有自动出现新表
因为逻辑复制不复制 DDL。
新表必须先在 B 库创建,再刷新 Publication。
十八、上线前检查清单
A 库
[ ] 当前数据库正确
[ ] wal_level = logical
[ ] max_replication_slots 足够
[ ] max_wal_senders 足够
[ ] 复制用户具备 REPLICATION
[ ] 复制用户具备 CONNECT、USAGE、SELECT
[ ] pg_hba.conf 仅允许 B 库来源
[ ] Publication 表清单正确
[ ] 发布表有主键或合理 Replica Identity
[ ] 复制槽纳入 WAL 和磁盘监控B 库
[ ] 目标 schema 已创建
[ ] 目标表结构与 A 库兼容
[ ] 目标表 owner 正确
[ ] 初次 copy_data=true 时目标表为空
[ ] Worker 参数足够
[ ] wal_receiver_timeout 合理
[ ] Subscription 已创建
[ ] subenabled = true
[ ] 所有表 srsubstate = r
[ ] 主 Apply Worker 正常
[ ] B 库业务账号无同步表写权限验证
[ ] INSERT 已验证
[ ] UPDATE 已验证
[ ] DELETE 已验证
[ ] A/B 抽样数据一致
[ ] 错误统计不再增长
[ ] 复制槽 active=true
[ ] retained_wal 在合理范围
[ ] 告警已配置十九、总结:逻辑复制真正难的不是创建,而是治理
PostgreSQL 逻辑复制的创建过程并不复杂:
A 库创建 Publication
B 库创建 Subscription但真正的生产难点集中在:
- 表结构兼容;
- 权限管理;
- 初始同步;
- DDL 变更;
- 复制槽 WAL 堆积;
- Worker 超时;
- 目标库写入冲突;
- 长事务和锁;
- 错误自动停用;
- 监控告警。
因此,一套成熟的逻辑复制方案不应该只包含部署命令,还应该包含:
复制范围设计
+
结构变更规范
+
故障排查手册
+
监控告警
+
安全权限
+
验收标准如果只记住一个原则,可以记住下面这句话:
逻辑复制负责持续同步数据变化,但表结构一致性、目标端写入控制和复制槽安全,仍然需要由运维体系负责。
对于“只同步部分业务表,同时让 B 库保持独立”的场景,PostgreSQL 原生逻辑复制是一种足够轻量、透明且可控的方案。
前提是:不要把它当作两条 SQL,而要把它当作一条需要持续治理的数据链路。
评论
游客无需注册即可评论。
你提交的昵称、邮箱、网址和评论内容会保存在服务端,用于展示评论身份、接收回复及必要的安全审计。
浏览器会本地保存已填游客信息和评论草稿,方便下次免填。
回复提醒会通过站内消息和邮件通知。