Skip to content

逻辑复制与流复制对比 ​

PostgreSQL 同时内置了两种复制机制,它们不是「新旧替代」关系,而是分工明确的两个工具:流复制(physical / streaming replication)搬运 WAL 字节流,目标是让备库成为主库的位级镜像;逻辑复制(logical replication)搬运解码后的行变更,目标是把「选定的数据」送到「需要的地方」。前面几页已经分别讲完了逻辑复制的原理、发布、订阅与限制,本页把两者放在同一张桌上逐维度对比,并给出选型决策树、混合架构与跨版本升级的实战思路。

做选型时最容易犯的两个错误,方向恰好相反:一是「以为逻辑复制能替代流复制做高可用」——它没有自动故障转移、不复制 DDL 与序列,扛不住「主库挂了马上接管」这个任务;二是「以为流复制也能做数据分发」——它没有粒度、不能跨大版本、备库只读,想把半个库的表喂给数仓是无从下手的。把两个工具的边界画清楚,架构讨论就省掉一半争论。

本页的结论先行:高可用的骨架用流复制,数据分发的血肉用逻辑复制。绝大多数中大型系统最终都会走到这个混合形态;纯流复制或纯逻辑复制,都只是这条路线上的子集。

前置知识

建议先阅读:逻辑复制限制

基础概念 ​

两种复制在机制层面的根本差异,决定了它们所有外在能力差异:

维度流复制(物理复制)逻辑复制
传输内容WAL 字节流(物理块变更)解码后的逻辑变更(表 + 行 + 操作)
目标端形态备库 standby,只读(hot standby)订阅端是普通实例,可读写
目标端数据形态与主库位级一致(页、块、文件布局)逻辑一致(行内容一致,物理布局无关)
一致性模型完整镜像「选定对象」的事务级一致
失败语义备库落后或断开,重连后继续重放apply 冲突会停住,需要人工介入

一个帮助记忆的类比:流复制像「磁盘镜像 / RAID 同步」,复制品连坏块都一致;逻辑复制像「应用层的数据同步」,复制品是重新组装出来的,结构可以不同、内容可以裁剪,但组装过程会出错、也需要原料清单(schema)齐全。

同一条 UPDATE,两条管道 ​

sql
UPDATE public.orders SET status = 'paid' WHERE order_id = 1001;

这条语句进入两种复制管道后的形态:

text
【流复制】WAL 物理记录:
    relation 16384 / block 42 / offset 0x1F
    「把该偏移起的 96 字节替换为:<tuple image>」
    —— 备库按块重放,不关心“这是一张订单表”。
       页结构、文件布局与主库完全一致。

【逻辑复制】解码后的逻辑变更:
    table public.orders ; action UPDATE
    replica identity key: order_id = 1001
    changed column: status = 'paid'
    —— 订阅端把它翻译成一条真正的 UPDATE 重新执行,
       走完整的事务、约束检查与索引维护路径。

前者快而「盲」,后者慢而「懂」。后面所有的维度差异——粒度、跨版本、DDL、序列、资源开销——都能追溯到这个本质区别。

两种复制不是互斥的 ​

同一实例可以同时运行两种复制:主库向备库发物理 WAL(保证高可用),同时向若干订阅端发逻辑变更(做分发)。官方文档也明确逻辑复制可以建立在「任何物理复制主库」之上。这正是「混合架构」的技术基础,见下文。

语法与用法 ​

对比两种复制各自的「最小启用成本」,能直观感受运维面的差别。配置参数详见各自的专题页与复制槽一页。

流复制的启用(发布端 + 备库):

conf
# 主库 postgresql.conf
wal_level = replica            # 默认值,无需改动
max_wal_senders = 10
bash
# 用基础备份拉起备库
pg_basebackup -h primary-host -p 5432 -U replicator -D /data/standby -X stream -P -R
# -R 会自动写入 standby.signal 与 primary_conninfo
conf
# 备库 postgresql.conf(或 postgresql.auto.conf)
primary_conninfo = 'host=primary-host port=5432 user=replicator password=*** application_name=standby1'
primary_slot_name = 'standby1_slot'
hot_standby = on               # 备库可读

逻辑复制的启用(发布端 + 订阅端):

conf
# 发布端 postgresql.conf —— wal_level 必须提升并重启
wal_level = logical
max_replication_slots = 10
max_wal_senders = 10
sql
-- 发布端
CREATE PUBLICATION pub_orders FOR TABLE public.orders, public.order_items;

-- 订阅端(表结构需先行手工同步)
CREATE SUBSCRIPTION sub_orders
    CONNECTION 'host=primary-host port=5432 dbname=app user=repl password=***'
    PUBLICATION pub_orders
    WITH (copy_data = true);

肉眼可见的差异:流复制几乎是「零配置默认可用」,备库的每一块数据都不用你操心;逻辑复制则要你亲手圈定范围、亲手同步结构、亲手处理版本与序列。

详细说明 ​

对比总表:15 个维度 ​

#维度流复制逻辑复制
1复制粒度集簇级,全库所有对象表级起步,可细到列清单、行过滤(PG15+)
2复制方向单向(主 → 备),支持级联单向为主;可多对一、一对多、级联、双向(需自制防回环与仲裁)
3目标端要求同大版本、同架构、位级镜像独立实例;schema 手工建;订阅端大版本 ≥ 发布端
4跨大版本不支持(必须同大版本)支持(旧发布 → 新订阅)
5DDL随 WAL 自动同步完全不复制
6序列随 WAL 自动同步不复制(切换前手工 setval)
7大对象随 WAL 自动同步不复制(官方明确,无内置绕行方案)
8源端性能开销极低:转发现成 WAL逻辑解码 CPU + reorder buffer 内存(logical_decoding_work_mem)
9目标端性能开销WAL 重放,速度快重新执行 DML(还要维护索引),速度慢
10延迟通常毫秒级通常高于流复制(解码 + 单 worker 串行应用)
11故障恢复成熟:promotion 即可接管;Patroni / repmgr 等工具自动化无内置 failover;PG17 槽同步到备库只是局部缓解
12拓扑灵活性一主多备、级联,角色固定分发、汇聚、级联、环形均可组合
13网络流量全库 WAL(含与业务无关的变更,可经 WAL 压缩减量)只发订阅内容;但文本格式逐行传输,宽表热点场景未必更省
14初始同步pg_basebackup 物理备份,位级一致快照table sync worker 逐表 COPY,各表快照时点不同
15零丢失承诺成熟的同步复制体系(synchronous_standby_names)可行但繁琐:订阅连接纳入同步列表 + 订阅参数 synchronous_commit 提升

复制粒度与目标端要求 ​

流复制的「全有或全无」既是缺点也是优点:不能只同步几张表,但也因此永不漏配——新加的表、新加的索引、新装的扩展,备库自动拥有。逻辑复制正好相反:每一张表都要显式进入发布(或命中 FOR ALL TABLES / schema 清单),schema 要手工同步,订阅端还可以「不像」发布端(多列、少索引、不同分区结构)。

目标端的可写性差异经常被低估:物理备库被强制只读,天然杜绝双写冲突;订阅端是普通实例,任何有权限的客户端都能写订阅表——灵活,但每一条本地写入都是未来冲突的种子。把订阅端当「可写的从库」用,是逻辑复制事故榜第一名。

「订阅端可以不像发布端」用一张图说明(流复制完全做不到这一点):

text
发布端 public.orders(OLTP 形态):
    列: order_id, user_id, amount, status, remark, ...
    索引: 主键 + 4 个业务索引(为在线交易服务)

订阅端 public.orders(数仓形态,同一张表):
    列: order_id, user_id, amount, status        ← 列清单只订阅 4 列
    索引: 仅 (created_at),表按月分区             ← 按分析查询模式重新设计
    行:  只保留近 3 年                            ← 行过滤裁剪历史

约束只有两条:发布 UPDATE/DELETE 时订阅端必须能定位旧行(复制标识覆盖的列要在订阅端存在),行过滤与列清单的规则要与发布端定义对得上;其余结构差异(多余的列、不同的索引、不同的分区方式)都是合法的。

DDL、序列、大对象的能力边界 ​

这三项的对照最能说明「位级镜像」与「逻辑重放」的差别,细节见逻辑复制限制:

对象流复制逻辑复制
CREATE TABLE / ALTER TABLE自动同步手工两端执行
CREATE INDEX自动同步手工(且订阅端可以按自己的查询模式建索引)
序列推进自动同步不复制,切换前手工同步
大对象自动同步不复制
角色 / 表空间 / GRANT自动同步不复制

注意「订阅端可以按自己的查询模式建索引」这一行:这是逻辑复制在数据分发场景的独特优势——数仓订阅端可以只为自己的查询建索引,不必背上 OLTP 索引的开销;流复制做不到这一点。

性能与资源消耗 ​

流复制的成本结构: 主库侧 walsender 只是转发现成的 WAL 字节,CPU 开销可忽略;网络上传送的是全库 WAL,包括与下游无关的变更(vacuum、索引维护、全库日志),这正是它「流量与业务量脱钩」的一面。备库侧重放 WAL,顺序写为主,吞吐远高于普通 DML。

逻辑复制的成本结构:

  • 发布端:每个订阅连接一个 walsender 做逻辑解码,这是真实的 CPU 开销;大事务在提交前要在 reorder buffer 里攒变更(受 logical_decoding_work_mem 控制,超出落盘);长事务会拖住槽的推进,让 pg_wal 持续滞留;
  • 网络:按行传输变更,默认文本格式(binary = true 可改二进制,PG14+),热点宽表每行都要带上复制标识的旧值(REPLICA IDENTITY FULL 时是整行);
  • 订阅端:每个订阅一个 apply worker 串行按事务顺序重新执行 DML(streaming = parallel 的并行只针对在途大事务,不改变整体串行边界),每个变更都要走完整的索引维护、约束检查路径——这是「重放永远比重放 WAL 慢」的根源。

一句话:流复制花的是「带宽」,逻辑复制花的是「两端的 CPU 与订阅端的写入能力」。当订阅端写入吞吐追不上发布端变更速率时,逻辑复制的延迟会持续放大且无法靠加硬件线性解决。

容量估算的四条经验法则(数值因业务而异,仅作起点):

  • 发布端解码开销按「每秒变更行数」估算:简单行每秒数万级通常单核可扛;宽表、TOAST 大值、大事务会显著放大开销;
  • 订阅端 apply 吞吐以「单 worker 串行重放」为上限估算,与本地同负载的写入性能同量级——订阅端磁盘与索引质量决定上限;
  • 大事务(单事务数十万行以上)两端都要专门评估:发布端 reorder buffer 攒积与落盘、订阅端串行重放,是延迟尖峰的最大来源;
  • 网络账单分开算:流复制按全库 WAL 字节计(可压缩),逻辑复制按行计,宽表热点场景后者未必更省。

延迟与故障恢复 ​

流复制的延迟模型简单:pg_stat_replication 里 sent_lsn 与 replay_lsn 的差值即滞后量,重放速度快,正常情况毫秒级;主库宕机时备库 promotion 即接管,RTO 可以做到秒级(配合外部工具全自动)。同步复制(synchronous_commit 各级别)还能提供零丢失承诺。

逻辑复制的延迟模型复杂得多:解码 → 传输 → apply 三段都有各自的排队;apply 端冲突会让链路完全停住而不是「慢一点」;初始同步阶段延迟以分钟到小时计。故障恢复更没有开箱即用的方案——发布端宕机意味着逻辑槽(和其中的解码状态)随实例消失,订阅端失去进度锚点。PG17 引入了订阅 failover 选项与槽同步机制(把逻辑槽状态随物理复制同步到备库,切主后由新主接管),算是补上了「发布端也是流复制集群」这一层的短板,但它的前提仍然是发布端自己先有流复制 HA——高可用的核心担当没有变。

复制槽:两种机制各自的锚点 ​

两种复制都靠复制槽记录「下游消费到哪里」,但槽的语义并不相同:

物理槽(流复制)逻辑槽(逻辑复制)
进度锚点restart_lsn:备库还需要的最早 WALconfirmed_flush_lsn:订阅端确认消费到的 LSN
滞留影响备库断线期间 WAL 无法回收同样阻止 WAL 回收,且 apply 停摆(冲突)时不像断线那样显眼
故障语义主库宕机后备库提升、重连新主即可继续主库宕机,槽状态随实例丢失,订阅链路断锚(PG17 起可同步到备库)
建槽方式pg_basebackup 自动或手工 pg_create_physical_replication_slot()订阅自动创建,或 pg_create_logical_replication_slot(name, 'pgoutput')

运维上最重要的差异在第三行:流复制的「恢复路径」内建在拓扑里,逻辑复制的恢复路径要人为设计。

初始同步方式对比 ​

流复制逻辑复制
方式pg_basebackup / 物理基础备份table sync worker 逐表 COPY
数据范围整个集簇(所有库、所有对象)仅发布范围内的表(PG15+ 订阅端应用行过滤 / 列清单)
一致性备份点的一致快照,位级一致各表快照时点不同,无跨表全局一致快照
对发布端压力一次集中 IO每张表一轮全表扫描 + 一个临时逻辑槽
目标端就绪速度取决于数据总量与网络取决于表数量与单表大小,可并行加速

同步提交与零丢失 ​

流复制有成熟的同步复制体系:主库 synchronous_standby_names 点名备库,事务按 synchronous_commit 级别(remote_apply / remote_write / on)等待确认,可承诺 RPO = 0,机制详见同步提交。

逻辑复制也能拼出同步链路:把订阅连接(其 application_name)纳入发布端 synchronous_standby_names,并把订阅参数 synchronous_commit 从默认 off 提升到 local 或更高。但工程上很少这么做——逻辑链路依赖订阅端 apply 的健康度,apply 停滞会让发布端写入被同步等待拖住,故障面比物理同步大得多。

conf
# 同步逻辑复制的最小配置(发布端 + 订阅端,仅供理解机制,生产慎用)

# 发布端:把订阅连接的 application_name 纳入同步列表
synchronous_standby_names = 'FIRST 1 (sub_sync_dw)'
sql
-- 订阅端:apply 事务按 local 级别响应(订阅参数,不是全局 GUC)
CREATE SUBSCRIPTION sub_sync_dw
    CONNECTION 'host=primary-host dbname=app user=repl application_name=sub_sync_dw'
    PUBLICATION pub_app
    WITH (synchronous_commit = local, copy_data = false);

这段配置之所以「慎用」,在于等待方是 apply worker 而不是 WAL 接收进程:订阅端一次 apply 冲突停摆,等于发布端全体写入挂起——物理同步复制里对应的故障(备库断线)有更成熟的检测与降级路径。

拓扑灵活性 ​

这是逻辑复制的绝对主场:

拓扑流复制逻辑复制
一主多备原生支持支持(多个订阅)
级联支持(备库再挂备库)支持(订阅端再建发布)
多对一汇聚不支持支持(多个发布端写向一个订阅端的不同表)
按表/按列/按行分发不支持支持(发布 + 行过滤 + 列清单)
双向同步不支持技术上可建链路,但仲裁需自制,谨慎
异构平台(Linux → Windows 等)不支持(位级依赖)支持

级联与容灾半径 ​

围绕「离源库越来越远」的场景,两种机制的答案不同:

需求流复制的答案逻辑复制的答案
备库再挂备库(级联,减轻主库压力)原生支持(级联备库)订阅端再建发布,向下一级订阅
跨机房容灾异步流复制,带宽按全库 WAL 评估只同步选定表,带宽小得多
防误删的延迟副本延迟备库(recovery_min_apply_delay)无内置延迟应用机制
下游再分发(扇出)备库只读,无法再分发物理流订阅端可再建发布,天然扇出

级联的另一层含义是职责分层:物理级联负责「保命」(离得越近的副本越完整),逻辑级联负责「供数」(越往下游越贴近消费场景)。两者的级联深度都应该有上限——每多一跳,延迟与排障难度都在叠加。

容灾半径的边界也要认清:流复制的灾备副本是「随时可提升为完整主库」的,RTO 由架构决定;逻辑复制的下游是「挂了重灌」的,灾备演练的目标是让重灌(初始同步 + 追平)时间可预估。给下游链路定 RTO 指标时,先算清「全量重灌要多久」,而不是照抄主库的秒级目标。

运维复杂度 ​

流复制的运维面:pg_stat_replication、复制槽滞留、promotion 演练。概念少、行为可预测,故障九成是网络与磁盘。

逻辑复制的运维面:发布定义、订阅参数、两端 schema 对齐、冲突处理 runbook、序列切换脚本、槽滞留监控、worker 进程池。概念多、状态机多,而且部分故障需要人工决策(冲突保留哪边)而不是重启就好。用订阅与限制两页的检查单把流程固化,是控制复杂度的唯一办法。

sql
-- 两层监控的最小 SQL 集(各留一条,覆盖八成的日常巡检)
-- HA 层:主备链路状态与重放延迟
SELECT application_name, state, sync_state,
       pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn) AS replay_lag_bytes
FROM pg_stat_replication;

-- 分发层:订阅表同步状态与错误计数
SELECT s.subname, r.srrelid::regclass AS table_name, r.srsubstate,
       st.latest_end_lsn, stats.apply_error_count
FROM pg_subscription s
LEFT JOIN pg_subscription_rel r ON r.srsubid = s.oid
LEFT JOIN pg_stat_subscription st ON st.subid = s.oid AND st.worker_type = 'apply'
LEFT JOIN pg_stat_subscription_stats stats ON stats.subid = s.oid;

选型决策树 ​

text
需求入口
│
├─ 主库宕机要快速/自动接管(高可用)?
│   └─ 是 ──► 流复制(+ 手工切换或 Patroni / repmgr 自动切换)
│
├─ 跨大版本升级或迁移(如 PG16 → PG18)?
│   └─ 是 ──► 逻辑复制
│
├─ 只需部分表 / 部分列 / 部分行到下游?
│   └─ 是 ──► 逻辑复制(发布 + 行过滤 + 列清单)
│
├─ 多个源库数据要汇聚到一个库?
│   └─ 是 ──► 逻辑复制(多对一)
│
├─ 下游要按自己的方式建索引、建结构?
│   └─ 是 ──► 逻辑复制
│
├─ 只是要一个完整的只读副本(读写分离 / 报表)?
│   └─ 是 ──► 流复制(零 DDL 负担、延迟最低)
│
├─ 异地灾备(接受位级镜像)?
│   └─ 是 ──► 流复制(异步;防误删用延迟备库)
│
└─ 双向同步 / 多活写入?
    └─ 谨慎 ──► 逻辑复制 + 自制仲裁,或改用单写架构

读这棵树的三个要点:它是需求向工具的判定,不是能力排名,每个分支指向「该需求下唯一或最优的工具」;前三个分支命中后基本可以定案,走到中段出现多个「是」的,通常是混合架构(流复制打底 + 逻辑复制分发);第一问「主库宕机要快速/自动接管」对 HA 需求零容忍——答不出秒级接管方案的,一律先补流复制层再谈分发。

混合架构:流复制做 HA + 逻辑复制做分发 ​

生产上最稳健的形态是两层叠加:

text
        ┌─────────────── 第一层:流复制(HA 骨架) ───────────────┐
        │                                                        │
        │   ┌──────────────┐    WAL 字节流    ┌───────────────┐   │
        │   │  主库 (写)    │ ──────────────► │ 物理备库 (只读) │   │
        │   │  PG18 primary │                 │  PG18 standby │   │
        │   └──────┬───────┘                 └───────────────┘   │
        │          │ failover 时备库提升为主,逻辑复制在新主重建/接管  │
        └──────────┼─────────────────────────────────────────────┘
                   │
        ┌──────────┴───────── 第二层:逻辑复制(分发血肉) ──────────┐
        │  publication: pub_dw(显式表清单)                         │
        │       ├──► 订阅端 A:数仓(列清单瘦身 + 行过滤)             │
        │       ├──► 订阅端 B:搜索索引构建服务                       │
        │       └──► 订阅端 C:审计归档(publish = 'insert')        │
        └───────────────────────────────────────────────────────────┘

落地要点:

  • 逻辑发布的建立位置要跟「写入口」绑定:挂在主库上,failover 后在新主上重建(PG17+ 配合 failover 槽同步可自动接管);
  • 分发订阅只消费业务表,与 HA 层完全解耦,数仓链路抖动不影响主备切换;
  • HA 层的 RTO 目标由流复制承担,分发层的延迟与冲突由各自的 runbook 承担,不要把两层的 SLO 混在一起谈;
  • 分发订阅的初始同步安排低峰执行,避免与 HA 演练、备份窗口重叠;
  • 每条分发链路独立评估:表清单、过滤条件、冲突预案、负责人,不要「一套配置喂所有下游」。

跨版本升级实战(PG16 → PG18) ​

逻辑复制最经典的单点用途:近零停机的大版本升级。与 pg_upgrade(需停写窗口、版本跨度受限)相比,逻辑复制方案在迁移期间旧库持续提供服务,只在最终切换时短暂停写。官方文档在「Logical Replication Upgrade」一节确认了这一用法:升级方向必须是「旧发布端 → 新订阅端」。

text
阶段 0  旧库(PG16):wal_level=logical(需重启)→ 建 repl 角色 → GRANT SELECT
阶段 1  旧库:CREATE PUBLICATION pub_upgrade FOR ALL TABLES;   -- 或按 schema/表清单
阶段 2  新库(PG18):pg_dumpall --roles-only 导入角色 → 建库 →
        pg_dump --schema-only 导入结构 → 安装扩展
阶段 3  新库:CREATE SUBSCRIPTION ... WITH (copy_data = true);  -- 全量 + 增量
阶段 4  等待追平:pg_subscription_rel 全部 'r',latest_end_lsn 稳定贴近发布端
阶段 5  切换窗口(分钟级):
        应用停写 → 等待 LSN 完全追平 → 序列 setval 同步 →
        应用连接串切到新库 → 冒烟验证
阶段 6  收尾:DROP SUBSCRIPTION(或 SET (slot_name = NONE) 保留槽)→
        旧库 DROP PUBLICATION → 旧库下线或转只读归档

与 pg_upgrade 的取舍:pg_upgrade 一次搞定全库所有对象、结构零漂移,但必须停写且大表场景窗口不可控;逻辑复制升级把停机压缩到分钟级,代价是全流程要人工维护(结构、序列、扩展),且大对象、全局对象要额外处理。两种方案的共同底线是:切换演练至少完整走一遍。

三种升级路径的横向对比:

pg_upgrade逻辑复制升级pg_dump 重放
停写窗口小时级,与库大小成正比分钟级(仅最终切换)小时级(导出 + 导入全程)
结构漂移风险零(位级迁移)有(schema 手工维护)有(导出重建)
回退难度低(旧库原样保留)低(切回旧连接串)低
大对象支持不复制,需另行处理支持(随导出)
附加成本版本跨度受限wal_level = logical 需重启、双端规划慢,全量重导
适用场景中小库、允许停机窗口大库、近零停机要求顺带重塑 schema 的重建

多活/多主的可行性与风险 ​

逻辑复制让「A、B 两库互为发布与订阅」在技术上成立(配合 origin = none 防回环),但工程上必须清楚三个风险:

  1. 冲突无仲裁:两侧同时更新同一主键,insert_exists / update_exists 会让其中一侧 apply 停住,谁来赢全靠人工;
  2. 回环风险:origin 过滤配置错一处,变更就在环上无限放大;
  3. 序列与全局状态:两侧序列各自推进,接管时必然要对账。

因此官方态度是「无内置冲突解决」。可行路径按推荐度排序:

  • 单写 + 分发(推荐):每张表只在一个节点可写,其余节点只读订阅;
  • 按表拆分写入权:不同业务表归不同节点写,表级单写;
  • 真双写:仅在接受「外部仲裁工具 + 一致性对账」成本时考虑,PostgreSQL 本身不提供该能力。

为什么逻辑复制不适合做高可用 ​

把前文结论收拢成清单,逐条对应高可用的硬指标:

高可用要求逻辑复制的现状
自动故障转移无内置机制;复制槽长在发布端,主库崩溃即丢失(PG17 槽同步只是缓解,且前提是发布端自己有流复制 HA)
秒级 RTO初始同步慢、apply 串行、冲突会停链路,RTO 不可控
结构完整性DDL 不复制,新主结构可能缺东少西
零丢失无成熟同步复制体系;理论上可把订阅连接纳入 synchronous_standby_names,但链路健康度绑定订阅端 apply,故障面大,工程上很少这么用
切换即写序列不同步、订阅表可能已被本地写入污染

结论不是「逻辑复制不好」,而是分工:让流复制承担它擅长的位级一致与故障转移,让逻辑复制承担它擅长的裁剪、跨版本与分发。把 HA 的期望寄托在逻辑复制上,等于用错工具。

顺带澄清一个常见误读:PG17 的 failover 订阅选项与逻辑槽同步,解决的是「分发链路在主库切换后自动接管」,而不是「订阅端接管业务写入」——它让逻辑复制的运维更顺滑,但没有改变上表任何一行结论。真正承担故障转移的,永远是流复制层。

架构图对比(文字) ​

text
【流复制:一条线,一个镜像】

  主库 (primary) ──── WAL 字节流 ────► 备库 (standby, 只读)
      │                                     │
      │ crash                               │ promote
      ▼                                     ▼
    (下线)                          新主库(原备库接管)
  ※ 集群内所有对象位级一致;DDL/序列/大对象全自动跟随


【逻辑复制:一张网,多份裁剪后的副本】

  发布端 A ── pub_x ──► 订阅端 X(全量表)
      │
      └── pub_y ──► 订阅端 Y(行过滤 + 列清单)
  发布端 B ── pub_z ──► 订阅端 Y(多对一汇聚)
      ▲
      └──── 订阅端 Y 同时是下游的发布端(级联)
  ※ 每条链路独立选表、选列、选行;结构手工维护;冲突人工处理

选择建议表 ​

需求场景首选理由
主库高可用、自动切换流复制 + 切换工具位级一致、RTO 可控、生态成熟
读写分离只读副本流复制零维护、延迟最低、DDL 自动跟随
大版本升级(PG16 → PG18)逻辑复制近零停机,方向合法(旧 → 新)
部分表/列/行向下游供数逻辑复制发布 + 行过滤 + 列清单是唯一解
多源汇聚进数仓/中台逻辑复制多对一是流复制不具备的能力
下游按自身模式建索引与结构逻辑复制订阅端结构与发布端解耦
异构平台之间同步逻辑复制不依赖物理布局
异地灾备流复制(异步)简单、可靠、全量
防误删的延迟灾备流复制(延迟备库)备库延迟重放,误操作可回捞
审计/事件类只追加分发逻辑复制 publish = 'insert'最小发布面,零冲突
含大对象的库整体同步流复制大对象逻辑复制不搬运
序列强依赖业务的接管切换流复制序列随 WAL 自动同步,切换零脚本
双向同步/多活谨慎评估无内置仲裁,优先改单写架构

把整张表压成一句话:要「一样的」选流复制,要「裁剪过的 / 跨版本的 / 汇聚的」选逻辑复制,两个都想要就上混合架构。表里所有「首选逻辑复制」的行都有共同特征——下游不是「完整的第二个主库」,而是带着自己目的的消费者;所有「首选流复制」的行,诉求都是「与主库完全一致」。

实战示例 ​

示例一:为「主库 + 数仓 + 检索服务」设计完整拓扑 ​

需求:核心库 3 TB,高可用 RTO < 1 分钟;数仓需要 20 张核心表的 8 个列;检索服务需要全量订单表并按自己的方式建索引。

text
设计结论:
- HA 层:主库 + 2 个流复制备库(1 同步 + 1 异步),Patroni 管切换
- 分发层:
    publication pub_warehouse:20 张表,列清单裁剪,行过滤近 3 年数据
    publication pub_search:orders 表全量列
- 订阅端:
    数仓订阅:streaming = off(数仓不关心大事务延迟,链路简单优先)
    检索订阅:streaming = parallel,订阅端自建全文检索索引
sql
-- 发布端(主库)
CREATE PUBLICATION pub_warehouse
    FOR TABLE public.orders (order_id, user_id, amount, status, created_at)
        WHERE (created_at >= '2023-01-01'),
          TABLE public.order_items, public.users, public.products;

CREATE PUBLICATION pub_search FOR TABLE public.orders;
sql
-- 数仓订阅端(只读使用,禁写)
CREATE SUBSCRIPTION sub_warehouse
    CONNECTION 'host=primary-host port=5432 dbname=app user=repl password=***'
    PUBLICATION pub_warehouse
    WITH (copy_data = true, streaming = off, disable_on_error = true);

-- 检索订阅端
CREATE SUBSCRIPTION sub_search
    CONNECTION 'host=primary-host port=5432 dbname=app user=repl password=***'
    PUBLICATION pub_search
    WITH (copy_data = true, streaming = parallel);

这个拓扑里两层各司其职:主备切换不牵动订阅;数仓或检索链路出问题(冲突、停顿)也不影响高可用。

示例二:一次 PG16 → PG18 升级的切换脚本骨架 ​

bash
# ── 切换窗口前的常态检查(T-1 天)──
# 新库订阅端:确认所有表就绪
psql -h new-host -p 5432 -U postgres -d app \
    -c "SELECT srrelid::regclass, srsubstate FROM pg_subscription_rel WHERE srsubstate <> 'r';"
# 期望输出:0 rows
bash
# ── 切换窗口(T 时刻)──
# 1. 应用停写(维护页 / 连接池排空)
# 2. 确认旧库与新库 LSN 追平
psql -h old-host -p 5432 -U postgres -d app \
    -c "SELECT pg_current_wal_lsn();"          # 例如 0/2A3B7C00
psql -h new-host -p 5432 -U postgres -d app \
    -c "SELECT latest_end_lsn FROM pg_stat_subscription WHERE worker_type = 'apply';"
# 3. 序列同步(在旧库生成语句,新库执行)
psql -h old-host -p 5432 -U postgres -d app -Atc \
    "SELECT format('SELECT setval(%L, %s);', schemaname||'.'||sequencename, COALESCE(last_value, start_value))
     FROM pg_sequences WHERE schemaname NOT IN ('pg_catalog','information_schema') AND last_value IS NOT NULL;" \
    | psql -h new-host -p 5432 -U postgres -d app
# 4. 应用连接串切到 new-host,冒烟测试
# 5. 收尾
psql -h new-host -p 5432 -U postgres -d app \
    -c "ALTER SUBSCRIPTION sub_upgrade DISABLE; DROP SUBSCRIPTION sub_upgrade;"
psql -h old-host -p 5432 -U postgres -d app \
    -c "DROP PUBLICATION pub_upgrade;"

整个停写窗口通常只有「确认追平 + 序列同步 + 冒烟」这几分钟,这就是逻辑复制升级相对 pg_upgrade 的核心价值。

示例三:判断「该用哪个工具」的快速问答 ​

text
Q1:这个需求里,主库挂了谁来服务?
    → 「新主库」      → 这是 HA 需求,流复制打底
    → 「订阅端」      → 继续问 Q2

Q2:需要复制的是「整个实例」还是「一部分数据」?
    → 整个实例       → 流复制(备库)
    → 一部分         → 逻辑复制(发布裁剪)

Q3:订阅端需要写吗?
    → 不需要,纯只读 → 两者皆可;版本相同选流复制,跨版本/裁剪选逻辑复制
    → 需要写(非订阅表)→ 逻辑复制(订阅端是普通实例)

Q4:源与目标的大版本相同吗?
    → 相同           → 两种都可行,按 Q2/Q3 决定
    → 不同           → 逻辑复制,且订阅端版本 ≥ 发布端

示例四:给现有「一主一备」补上分发层的落地清单 ​

前提:已有流复制主备(PG18),要新增一个数仓订阅端。按顺序勾选执行:

text
□ 1. 主库确认 wal_level = logical(当前是 replica 的话,规划重启窗口调整)
□ 2. 主库创建复制角色 repl(LOGIN + REPLICATION),pg_hba 放行数仓网段
□ 3. GRANT SELECT 于待发布的表(初始同步需要)
□ 4. 数仓端建库、pg_dump --schema-only 建表、按数仓查询模式建索引
□ 5. 主库 CREATE PUBLICATION pub_dw(显式表清单,按需加列清单/行过滤)
□ 6. 数仓端 CREATE SUBSCRIPTION ... WITH (copy_data = true)
□ 7. 监控接入:pg_stat_subscription / pg_stat_subscription_stats / pg_replication_slots
□ 8. 复核 HA 层不受影响:pg_stat_replication 中主备链路正常,重跑一次切换演练
□ 9. 把「切主后在新主重建发布与订阅(或启用 PG17+ failover 槽同步)」写进切换手册

第 8 步最容易被忽略:新增的逻辑订阅会给主库增加持续的解码负载,上线后要重新评估主库与备库的资源水位,确认 HA 层的延迟指标没有劣化。

注意事项 ​

不要用「延迟」来比两者的优劣,要用「语义」来选型。 流复制慢在重放 WAL(快),逻辑复制慢在重新执行 DML(慢);但逻辑复制提供裁剪与跨版本能力,流复制提供完整镜像。拿错维度比较会得出错方案。

混合架构里,逻辑发布挂在主库上。 挂在流复制备库上做发布的思路看似减轻主库压力,但备库提升为主后链路归属会变乱;除非明确做级联分发,否则发布端就是写入口。

升级方案必须演练序列与大对象。 序列不同步是切换后第一批写入冲突的头号原因;大对象不复制意味着含大对象的库要另行处理,这两件事在方案评审时就要有结论。

多主不是「两条单向链路」这么简单。 防回环(origin = none)、冲突仲裁、序列对账、脑裂防护,四件事缺一件就是生产事故,缺省答案是「别做真多主」。

监控要分层。 HA 层看 pg_stat_replication 与备库重放延迟;分发层看 pg_stat_subscription、pg_stat_subscription_stats 与发布端槽滞留。两层的告警阈值、值班响应动作应当分开定义。

「备库只读」不是限制而是保险。 流复制备库被强制只读,从机制上杜绝了双写分歧;逻辑复制订阅端随手可写,等于把一致性风险交给了每一个有写权限的人。把订阅表的写权限收到只剩 apply worker,是混合架构的第一纪律。

评估逻辑订阅对主库的开销按「变更量」算,不按「库大小」算。 3 TB 的静态库可能解码开销近乎为零,100 MB 的高频更新表也可能吃掉一个核。容量评估的输入是每秒变更行数与大事务占比。

最佳实践 ​

  1. 默认架构 = 流复制 HA + 逻辑复制分发:先搭好 HA 骨架,再有分发需求时在主库上加发布,不要一开始就绕过 HA。
  2. 高可用永远交给流复制(配合 Patroni / repmgr 或手册化的 promotion 流程),逻辑复制不承担 RTO 指标。
  3. 分发链路的发布用显式表清单,与 FOR ALL TABLES 保持距离,让下游消费范围永远可审计。
  4. 订阅端按自己的查询模式建索引,不必镜像源端索引——这是逻辑分发的独有红利,也是数仓订阅端性能的关键。
  5. 跨版本升级把「追平判断、序列同步、切换、回退」写成脚本并演练,切换窗口的目标是分钟级,而不是「现场想办法」。
  6. 升级方向铁律:订阅端大版本 ≥ 发布端;规划升级路径时永远让新版本站下游。
  7. 多主需求先做架构否决:能单写就单写,能按表拆写就拆写;真要双写,把仲裁与对账的成本写进立项评估。
  8. PG17+ 的发布端如果本身有流复制备库,为订阅开启 failover 槽同步,让分发链路在切主后自动接管;旧版本则把「在新主重建发布与订阅」写进切换手册。
  9. 网络与资源规划分开算两笔账:流复制按「全库 WAL 带宽」算,逻辑复制按「解码 CPU + 订阅端写入吞吐」算,任何一笔超了都会拖垮链路。
  10. 监控面板分层呈现:HA 层(pg_stat_replication、WAL 滞留)与分发层(pg_stat_subscription、错误计数、槽滞留)分开展示,故障时先分层定位再下钻。
  11. 文档化每条复制链路的「负责人、SLO、冲突预案」:逻辑复制链路一旦停住需要人决策,没有 runbook 的链路等于没有链路。
  12. 每次大版本升级后重读一遍限制清单:官方在持续补齐短板(流式并行、两阶段、槽同步、生成列发布),旧认知需要随版本刷新。

下一节

继续学习:手工故障转移

参考链接 ​