1. 逻辑复制全景概览
PostgreSQL 的复制能力分为两条主线:物理复制与逻辑复制。物理复制以 WAL(Write-Ahead Log)为搬运对象,把主库磁盘页的字节级变化原样送到备库,因此备库是主库的“块级镜像”,只能整实例复制,无法只挑一张表。逻辑复制则不同,它把 WAL 解码成一行行的逻辑变更(INSERT/UPDATE/DELETE/TRUNCATE),再以表为单位投递到订阅端,由订阅端重新执行这些变更。理解这一层“字节流 vs 行变更”的差异,是掌握逻辑复制全部特性的起点。
逻辑复制的定义与本质
逻辑复制是 PostgreSQL 10 引入的原生特性,它基于发布/订阅模型:发布端定义“发布”(Publication),声明哪些表的哪些操作需要被复制;订阅端定义“订阅”(Subscription),声明从哪个发布端、哪个发布拉取数据。发布端把变更写入复制槽,订阅端通过逻辑复制协议拉取并应用。整个过程对表结构有要求——被复制的表必须有主键或唯一约束(用于定位 UPDATE/DELETE 的目标行),否则更新和删除无法正确路由。
与物理复制的本质区别可以归纳为四点:复制粒度上,物理复制是实例级,逻辑复制是表级;复制内容上,物理复制传 WAL 块,逻辑复制传解码后的行变更;版本兼容上,物理复制要求主备大版本一致,逻辑复制允许跨大版本(如 PG 14 发布到 PG 16 订阅);拓扑能力上,物理复制是单向流复制,逻辑复制支持多主汇聚、双向同步、部分表复制等更灵活的拓扑。
核心组件与数据流
逻辑复制链路上有五个关键角色。发布端侧:Publication 是变更的“筛选器”,决定哪些表、哪些操作(insert/update/delete/truncate)进入复制流;Replication Slot(逻辑复制槽)是变更的“缓冲区”,它记录订阅端已消费到的 LSN 位置,防止发布端过早回收 WAL;walsender 是发布端的发送进程,负责从复制槽读取变更并通过逻辑复制协议发给订阅端。订阅端侧:Subscription 是拉取配置,记录连接串、发布名、复制槽名;apply worker 是订阅端的应用进程,接收变更并在本地表上执行,同时把进度写回订阅端的复制源状态。
数据流的方向是:业务写入发布端表 → WAL 落盘 → 逻辑解码插件(默认 pgoutput)把 WAL 解码为行变更 → walsender 从复制槽读取并发送 → 订阅端 apply worker 接收并应用 → 订阅端表数据更新。这条链路中,复制槽是“背压”的关键:如果订阅端长时间不消费,复制槽会持续保留 WAL,导致发布端磁盘增长,这是运维中最需要监控的点。
典型应用场景
逻辑复制最直接的价值是跨版本升级:把 PG 12 主库的数据通过逻辑复制同步到 PG 16 新库,验证业务后再切换,停机窗口可以压缩到分钟级。其次是部分表复制:只把订单、用户等核心表同步到报表库或数据仓库,避免整实例复制带来的冗余。第三是多主汇聚:多个区域库各自写入本地表,通过逻辑复制汇聚到一个中心库做全局分析。第四是双向同步:两个库互为发布者和订阅者,实现双活写入,但需要处理冲突(如主键冲突、更新冲突),通常配合冲突解决策略或业务层规避。第五是异构同步:通过逻辑解码插件把变更投递到 Kafka、消息队列或其他数据库,构建 CDC 管道。
与物理复制的选型对照
| 维度 | 物理复制 | 逻辑复制 |
|---|---|---|
| 复制粒度 | 实例级(全部数据) | 表级(可挑选) |
| 复制内容 | WAL 块(字节级) | 行变更(逻辑级) |
| 版本要求 | 主备大版本一致 | 可跨大版本 |
| 拓扑能力 | 单向流复制 | 多主、双向、汇聚 |
| DDL 同步 | 自动同步 | 不自动,需手动执行 |
| 序列同步 | 自动同步 | 不自动,需单独处理 |
| 典型延迟 | 毫秒级 | 毫秒到秒级 |
关键参数与观察点
发布端需要设置 wal_level = logical,这是逻辑复制的前提,修改后需重启实例。复制槽的 restart_lsn 表示订阅端尚未确认消费的最早 WAL 位置,pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn) 可以估算积压的 WAL 字节数。订阅端可以通过 pg_stat_subscription 查看 apply worker 的状态、最后接收 LSN 和最后应用 LSN,两者的差值反映应用延迟。如果 last_msg_receipt_time 长时间不更新,说明订阅端没有收到新变更,需要检查网络或发布端 walsender 状态。
-- 发布端:查看复制槽积压
SELECT slot_name, plugin, slot_type, active,
pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS retained_wal
FROM pg_replication_slots
WHERE slot_type = 'logical';
-- 订阅端:查看应用进度
SELECT subname, pid, relid, received_lsn, latest_end_lsn,
now() - last_msg_receipt_time AS since_last_msg
FROM pg_stat_subscription;