Skip to content

级联复制 ​

级联复制(cascading replication)指的是让一台备库不只接收主库的 WAL,还把它接收到的 WAL 再转发给下游备库。此时这台中间的备库同时扮演「接收者」和「发送者」两个角色:对主库来说它是备库,对下游来说它是 WAL 来源。整个拓扑从「星形」(所有备库直连主库)变成「树形」。

引入级联的动机通常是资源约束。每个直连主库的备库都会在主库上占一个 walsender 进程,并消耗主库的出口带宽;当备库数量增长、或备库与主库之间跨越广域网时,主库很快会成为瓶颈。让一台「区域汇聚节点」先接收主库的 WAL,再分发给同区域的其他备库,可以把主库的出口流量压缩成一份,同时把跨地域的延迟集中在一段链路上。

但级联不是没有代价的。它引入了传播链:上游备库故障会波及所有下游;延迟会沿着链路叠加;复制槽需要在多个节点上分别维护;同步复制无法跨越级联使用。因此级联复制应当被看作一种「有明确收益时才采用」的拓扑优化,而不是默认开启的选项。本页会讲清它的配置、延迟模型、适用场景与限制。

前置知识

建议先阅读:pg_stat_replication 监控

基础概念 ​

级联复制改变了节点之间的关系,因此先要理清每个节点在拓扑中的角色。

角色说明
主库(primary / root)WAL 的最终来源,向上游唯一
中间备库(intermediate / relay)同时接收与发送 WAL 的备库
叶子备库(leaf / downstream)只接收、不再向下游发送的备库
上游(upstream)相对某个节点的 WAL 来源
下游(downstream)相对某个节点,接收其 WAL 的节点
传播深度从主库到某节点经过的转发层数

级联与普通流复制的差别 ​

维度普通流复制级联复制
拓扑星形(所有备库直连主库)树形(可多层)
主库 walsender 数量等于备库数等于第一层节点数
主库出口带宽与备库数成正比只与第一层节点数成正比
延迟一跳随深度叠加
故障影响面单台备库上游故障影响整棵子树
复制槽位置仅主库每对「上游-下游」都需要
同步复制可用不能跨越级联
配置复杂度低中(需规划层级与槽)

关键区别在于「主库的负担与备库数量解耦」。这是级联最大的价值,也是它唯一无法被其他方案替代的地方。

级联中的 WAL 流向 ​

在级联拓扑里,WAL 是这样流动的:主库产生 WAL → 通过 walsender 发给中间备库 → 中间备库的 walreceiver 接收、写入本地 pg_wal → 中间备库的 startup 进程重放 → 同时中间备库的 walsender 把同一份 WAL 发给下游备库。

text
                    ┌──────────────────────────────────────┐
                    │              主库 (primary)           │
                    │   walsender ──┐                      │
                    └───────────────┼──────────────────────┘
                                    │ ① 一份出口流量
                                    ▼
              ┌──────────────────────────────────────────┐
              │        中间备库 (relay / standby-A)       │
              │  walreceiver ──► pg_wal ──► startup(重放) │
              │                     │                    │
              │                     └──► walsender ──┐   │
              └──────────────────────────────────────┼───┘
                                    │ ② 分发          │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
        ┌────────────────────┐         ┌────────────────────┐
        │ 叶子备库 (standby-B) │         │ 叶子备库 (standby-C) │
        └────────────────────┘         └────────────────────┘

注意中间备库的重放与转发是并行的:它不需要等自己重放完才转发,而是从本地 pg_wal 中读取记录发送。这两件事共享同一份 WAL 文件,因此下游的进度可以领先于中间备库自身的 replay_lsn。

上游与下游的判定 ​

「上游」「下游」是相对概念。判断某台机器在拓扑中的位置,看它的配置即可:

观察结论
有 standby.signal,无下游连接叶子备库
有 standby.signal,且 pg_stat_replication 有行中间备库
无 standby.signal,pg_stat_replication 有行主库(或已提升的新主库)

因此在中间备库上,两个视图同时有内容是正常的:pg_stat_wal_receiver 有 1 行(它在接收),pg_stat_replication 也有若干行(它在发送)。

级联的延迟模型 ​

级联引入的额外延迟来自「每一跳都要完整走一遍接收 + 落盘 + 再发送」。总延迟近似为各跳延迟之和:

text
总延迟 ≈ 主库→第一层延迟 + 第一层→第二层延迟 + ... + 最后一跳延迟

每一跳的延迟又由该段的网络往返、落盘耗时、以及中间节点的负载共同决定。因此:

  • 两层的级联,延迟大约是直连的两倍;
  • 跨地域级联(例如主库在 A 地、汇聚节点在 B 地、叶子在 C 地)时,最后一跳可能在 B→C 上产生可观延迟;
  • 中间备库如果同时承担只读查询负载,可能因为查询冲突而拖慢自身重放,但不会直接拖慢转发(转发读的是 pg_wal,不依赖重放)。

最后一点很重要:级联的转发与重放解耦,意味着中间备库的查询负载不会直接放大下游延迟。但中间备库的磁盘 I/O 与 CPU 会同时被接收、重放、转发三件事消耗,间接影响所有环节。

级联中的复制槽 ​

级联拓扑里的每一对「上游 → 下游」关系,都可以(也建议)使用一个物理复制槽,而且这些槽位于上游节点的本地:

关系槽所在位置使用者
主库 → 中间备库主库中间备库
中间备库 → 叶子备库 B中间备库叶子备库 B
中间备库 → 叶子备库 C中间备库叶子备库 C

这意味着中间备库上也会有复制槽,并且同样需要规划 max_replication_slots 与 max_slot_wal_keep_size。这是级联配置里最容易被忽略的一点:很多人在主库上精心配置了槽与上限,却忘了中间层也需要同样的保护。

级联与同步复制的限制 ​

同步复制只能作用于直连主库的备库。 也就是说,级联中的中间备库可以被声明为同步备库(因为它直连主库),但叶子备库不能——主库根本不知道它们的存在,也就无法等待它们的确认。

因此,如果业务要求 RPO = 0,级联的拓扑必须保证「同步备库直连主库」。一种常见做法是:在同一个机房内让一台备库直连主库并作为同步备库,异地备库则通过级联挂在它下面,接受更高的延迟但零丢失保障由同机房节点提供。

级联与故障切换的关系 ​

级联拓扑下的故障影响面比星形大,这是它的主要代价:

故障节点影响
主库所有节点都需要切换(与星形一致)
中间备库其下所有叶子备库的复制中断
叶子备库只影响自己

因此中间备库的可靠性要求实际上高于叶子备库,应当给它更好的硬件与更严格的监控。同时,故障切换后的拓扑重建也更复杂:中间备库如果被提升为新主库,它的下游可以继续跟随;但如果中间备库需要重建,下游节点也要重新指向新的上游。

级联的适用场景 ​

级联并非总是更好。以下场景适合采用:

  • 跨地域分发:主库在一个地域,多个地域各有若干备库。每个地域设一个汇聚节点,减少跨地域流量。
  • 备库数量多:例如 10 台以上备库时,主库的 walsender 与出口带宽成为瓶颈。
  • 只读扩展:为读多写少的场景部署多个只读副本,用级联降低源节点压力。
  • 降低源节点负载:备份、分析类工作负载集中在子树上,避免影响源节点。

以下场景不适合采用:

  • 备库数量很少(1~2 台):直接连接更简单,级联只增加复杂度。
  • 要求 RPO = 0:级联无法提供跨层的同步确认。
  • 对延迟极度敏感:每一跳都会叠加延迟。
  • 运维能力有限:级联的故障排查与拓扑重建难度明显更高。

语法与用法 ​

级联的配置在叶子备库侧与「普通备库」完全一样——区别只在于 primary_conninfo 里填的上游不是主库,而是中间备库。

第一步,中间备库按普通备库方式搭建(连接主库):

bash
# 在中间备库上:正常连主库
pg_basebackup -h 192.168.1.10 -U repl -D /var/lib/postgresql/18/data \
              -Fp -Xs -P -R --slot=relay_slot
ini
# 中间备库的 postgresql.auto.conf
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl application_name=relay-a'
primary_slot_name = 'relay_slot'

第二步,让中间备库具备「发送能力」。这些参数必须设置,否则它无法为下游提供 WAL:

ini
# 中间备库上的 postgresql.conf
wal_level = replica              # 至少要 replica,才能向下游发送
max_wal_senders = 10             # 中间节点的发送名额,容纳其下游数量
max_replication_slots = 10       # 中间节点自己的槽上限
hot_standby = on                 # 保留只读能力

第三步,在中间备库上创建给下游用的复制槽:

sql
-- 在中间备库上执行(注意:不是主库)
SELECT pg_create_physical_replication_slot('leaf_b_slot');
SELECT pg_create_physical_replication_slot('leaf_c_slot');

第四步,放行下游的复制连接(中间备库上的 pg_hba.conf):

ini
# 中间备库的 pg_hba.conf:允许下游网段做复制
host    replication    repl    192.168.2.0/24    scram-sha-256

第五步,叶子备库从中间备库拉取基础备份,把 primary_conninfo 指向中间备库:

bash
# 在叶子备库 standby-B 上:注意 host 指向中间备库
pg_basebackup -h 192.168.1.30 -U repl -D /var/lib/postgresql/18/data \
              -Fp -Xs -P -R --slot=leaf_b_slot
ini
# 叶子备库的 postgresql.auto.conf
primary_conninfo = 'host=192.168.1.30 port=5432 user=repl application_name=leaf-b'
primary_slot_name = 'leaf_b_slot'

验证级联是否生效(在中间备库上):

sql
-- 中间备库:既有接收(1 行),也有发送(N 行)
SELECT 'receiver' AS side, status, slot_name FROM pg_stat_wal_receiver
UNION ALL
SELECT 'sender', state, application_name FROM pg_stat_replication;
text
   side   |  status   |    slot_name
----------+-----------+----------------
 receiver | streaming | relay_slot
 sender   | streaming | leaf-b
 sender   | streaming | leaf-c
(3 rows)

详细说明 ​

级联的配置步骤拆解 ​

把上面的步骤整理成一个完整清单,便于按序执行与核对。

步骤位置操作
1主库确认 wal_level = replica、max_wal_senders 充足
2主库创建中间备库用的物理槽
3主库pg_hba.conf 放行中间备库
4中间备库pg_basebackup -R 从主库拉基础备份
5中间备库配置 primary_conninfo + primary_slot_name
6中间备库设置 wal_level / max_wal_senders / max_replication_slots
7中间备库创建下游用的物理槽
8中间备库pg_hba.conf 放行下游网段
9叶子备库pg_basebackup -R 从中间备库拉基础备份
10叶子备库配置 primary_conninfo + primary_slot_name
11全部验证两侧视图,确认链路贯通

容易漏掉的是第 6 步:很多人只把中间备库当「备库」配置,忘了它同时需要「发送方」的参数。如果 wal_level 是 minimal(在旧配置里可能存在),或者 max_wal_senders = 0,中间备库就无法为下游提供 WAL,表现为叶子备库连不上或立刻断开。

每层都需要复制槽的原因 ​

复制槽解决的是「上游如何知道下游需要保留到哪个位置」。在级联里,每一对上下游关系都是独立的,因此每一对都需要自己的槽:

  • 主库通过 relay_slot 知道中间备库的进度;
  • 中间备库通过 leaf_b_slot 知道叶子备库 B 的进度;
  • 中间备库通过 leaf_c_slot 知道叶子备库 C 的进度。

如果不给下游配置槽,中间备库就只能靠 wal_keep_size 兜底,那么「叶子备库落后过多导致 WAL 缺失」的风险依然存在。因此级联拓扑里的槽数量是「边数」,不是「节点数」。

这也带来一个容量影响:中间备库要同时为「自己重放」和「下游落后」保留 WAL。它的 pg_wal 占用会比普通备库更大,磁盘规划时必须计入。

延迟叠加的量化 ​

假设三段链路的单向延迟分别是:

链路单向延迟
主库 → 中间备库20 ms
中间备库 → 叶子备库 B5 ms
中间备库 → 叶子备库 C80 ms(跨地域)

那么:

  • 中间备库的重放延迟约在 20 ms 量级(外加落盘与重放耗时);
  • 叶子备库 B 约 25 ms 量级;
  • 叶子备库 C 约 100 ms 量级。

这解释了为什么跨地域级联要把「汇聚节点」放在靠近叶子的一侧,而不是靠近主库一侧——把最贵的跨地域链路只走一次,用它去覆盖更大范围的叶子节点,是级联的核心收益。

实际延迟还受「批量发送」影响:WAL 通常不是逐条发送,而是按批次推送。因此小事务密集的场景下,单跳的实际感知延迟可能明显高于理论链路延迟。

复制槽与磁盘的连锁风险 ​

级联拓扑下,复制槽的磁盘风险会被放大,因为存在「级联失效」的可能:

  1. 叶子备库 C 长期掉线;
  2. 中间备库的 leaf_c_slot 不断保留 WAL;
  3. 中间备库的 pg_wal 持续增长;
  4. 中间备库磁盘写满或被迫截断 WAL;
  5. 中间备库既无法重放,也无法为其他下游提供 WAL,A 和 B 也一起受影响。

这构成了一个「一个叶子的故障波及整棵子树」的故障链。防范手段:

  • 在中间备库上配置 max_slot_wal_keep_size,给每个槽设上限;
  • 在 PostgreSQL 18 上考虑启用 idle_replication_slot_timeout,自动失效长期不活跃的槽;
  • 给中间备库的 pg_wal 目录设置比普通备库更严格的容量告警;
  • 对叶子备库的可用性做主动监控,掉线后及时处置而不是任其悬挂。

故障传播与影响面 ​

级联的故障传播方向是「自上而下」的,理解这一点有助于设计拓扑与制定应急流程。

故障直接后果二次后果
主库故障所有节点停止接收新 WAL需要切换,见下节
中间备库故障叶子节点的复制全部中断中间备库的槽保留 WAL,可能撑爆其 pg_wal(若它还能启动)
叶子备库故障仅该节点中断中间备库对应槽保留 WAL
网络(主库-中间)中断整棵树延迟增长叶子节点仍能从中间备库取「已有的」WAL,之后一起停滞
网络(中间-某叶子)中断仅该叶子停滞中间备库对应槽保留 WAL

值得注意的是「主库-中间链路中断」这个场景:由于中间备库本地有 pg_wal,它可以继续向叶子节点提供「已经收到但还没转发完」的 WAL。这在一段时间内让叶子节点看起来还正常,但一旦本地 WAL 发完,所有叶子会同时停滞。这个「延迟显现」的特性会让故障被滞后发现,因此对中间备库与主库之间链路的监控必须敏感。

提升中间备库后的拓扑处理 ​

如果中间备库被提升为新主库(例如主库永久故障),拓扑会变成:

text
        中间备库(提升后成为新主库)
              │
      ┌───────┴───────┐
      ▼               ▼
  叶子备库 B       叶子备库 C

此时:

  • 叶子备库 B / C 原本就连接中间备库,不需要改动配置即可继续复制——这是级联在切换后的一个便利。
  • 但叶子备库在提升瞬间的重放位置可能落后于中间备库,需要在提升前确认其延迟可接受(或在切换流程中等待其追平)。
  • 如果原主库还能恢复,它需要作为新的备库重新接入(通常需要 pg_rewind 或重做基础备份),并注意时间线已经递增。

这一特性说明:级联拓扑在「主库故障」场景下,切换动作反而可能比星形更简单,因为下游连接的上游没变。

级联与其他方案的组合 ​

级联常常与其他机制组合使用:

组合说明
级联 + 延迟备库叶子节点设置为延迟重放,作为误操作恢复的「时间闸门」,且不影响上游
级联 + 只读扩展一个区域内的多个只读副本挂在汇聚节点下,减少源节点压力
级联 + 归档只有主库需要归档即可,叶子节点可从上游或归档补齐(取决于配置)
级联 + 同步复制只能对直连主库的节点使用同步;叶子为异步
级联 + 逻辑复制逻辑订阅通常直连发布者,不参与物理级联

其中「级联 + 延迟备库」是比较实用的一种:把延迟放在叶子节点上,既能保护上游不受影响,又能保留一个可回退的时间窗口。

级联的监控要点 ​

级联的监控比星形复杂,因为需要「逐层检查」。基本方法是在每个节点上都确认它的上下游都健康:

节点类型需要看的视图关注的异常
主库pg_stat_replication + pg_replication_slots与中间备库的连接是否健康
中间备库pg_stat_wal_receiver + pg_stat_replication + pg_replication_slots接收是否正常、每个下游是否在发送、槽是否堆积
叶子备库pg_stat_wal_receiver + pg_last_wal_replay_lsn()接收与重放是否正常

一个实用的做法是:在每台节点上采集「自己接收侧的延迟」与「自己发送侧的延迟」,然后在监控系统里按拓扑关系拼成一条链路视图。这样出现问题时能立刻判断「是这一段慢了,还是上游整体慢了」。

级联的层数限制与实践建议 ​

PostgreSQL 本身对级联深度没有硬性限制,理论上可以有很多层。但实践上强烈建议不超过两层(主库 → 中间 → 叶子),原因:

  • 延迟逐层叠加,多层后业务已经无法接受;
  • 故障排查的复杂度随层数指数上升;
  • 每多一层就多一组复制槽与一套参数需要维护;
  • 单点故障的影响面在多层拓扑下更难评估与规避。

如果确实需要更深的分发层次,应当评估是否更适合用逻辑复制、或使用专门的复制中间件。

级联拓扑下的时间线管理 ​

级联复制让「时间线(timeline)」这件事变得更容易被忽略,而它恰恰是级联环境里最容易出问题的部分。

回顾一下时间线的机制:每次主库发生故障切换(提升一个备库),新主库会创建一个新的时间线 ID。所有下游节点必须跟着切换到同一条时间线,否则它们的数据会与新主库分叉,之后再也无法继续接收新主库的 WAL。

在级联拓扑下,一次提升涉及的时间线传播是逐级的:

  • 被提升的节点(无论它是中间备库还是叶子备库)产生新时间线;
  • 它的直接下游需要跟随新时间线,通常是通过 recovery_target_timeline = 'latest'(PG12 之前的默认值是 'latest',PG12 起同样是 'latest')自动完成;
  • 更下游的节点则依赖中间节点把新时间线的 WAL 继续转发下去。

几个实践要点:

  1. 提升中间备库是级联拓扑里最危险的操作。 一旦中间节点被提升,它下面挂着的所有叶子节点都必须把 primary_conninfo 改指向它,并可能需要重建;如果只是想让中间节点重新跟随原主库,则必须用 pg_rewind 或重建来处理时间线分叉。
  2. 叶子节点的 recovery_target_timeline 应当保持默认的 'latest',这样上游切换时间线后它能自动跟上,不需要人工干预。
  3. 归档(如果启用了)要按时间线分别保存。 archive_command 产生的归档文件名包含时间线 ID,混合存放在同一目录是可行的,但归档清理策略(pg_archivecleanup)必须明确以哪条时间线为准。
  4. 故障切换后要复核每个节点的 pg_walfile_name(pg_last_wal_replay_lsn()),确认所有节点回报的时间线 ID 一致,这是判断「拓扑是否仍然自洽」的最快方式。

关于时间线切换的完整流程与 pg_rewind 的使用,参见时间线管理与pg_rewind 修复。

级联与同步复制的组合 ​

级联拓扑本身与同步复制并不冲突,但组合起来会带来一个容易踩的坑:synchronous_standby_names 里列出的名字,必须是直接接收 WAL 的那一级节点。

也就是说,如果你配置的是「主库 → 中间备库 → 叶子备库」,那么主库上的 synchronous_standby_names 只能引用中间备库的 application_name,而不能直接引用叶子备库。叶子备库对主库而言是不可见的,它的 application_name 出现在中间备库的 pg_stat_replication 上,而不是主库上。

这带来两个实际影响:

  • 同步复制保证的是「主库 → 中间备库」这一跳的持久性,中间备库之后再转发给叶子节点的过程仍然是异步的;因此「同步」并不等于「数据已经在所有叶子节点上落地」。
  • 如果想要更强的保证,可以考虑在中间备库上也配置同步复制,让它把「叶子节点已收到」作为自己向主库反馈的条件之一——但这会进一步放大延迟,实践中很少这样做。

选择级联 + 同步复制组合时,应当明确「同步这个词覆盖到拓扑的哪一段」,避免在故障切换时才发现预期与实际不符。本系列后续章节会详细展开同步复制的配置方式。

实战示例 ​

示例一:搭建两层级联拓扑 ​

完整走一遍「主库 → 中间备库 → 两个叶子备库」的搭建。

bash
# 1. 主库上创建中间备库的槽
psql -h 192.168.1.10 -U postgres \
  -c "SELECT pg_create_physical_replication_slot('relay_slot');"
bash
# 2. 中间备库从主库拉基础备份
pg_basebackup -h 192.168.1.10 -U repl -D /var/lib/postgresql/18/data \
              -Fp -Xs -P -R --slot=relay_slot
ini
# 3. 中间备库配置(接收 + 发送)
# postgresql.auto.conf
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl application_name=relay-a'
primary_slot_name = 'relay_slot'

# postgresql.conf
wal_level = replica
max_wal_senders = 10
max_replication_slots = 10
max_slot_wal_keep_size = 10GB
sql
-- 4. 中间备库上创建下游槽
SELECT pg_create_physical_replication_slot('leaf_b_slot');
SELECT pg_create_physical_replication_slot('leaf_c_slot');
bash
# 5. 叶子备库从中间备库拉基础备份(host 指向中间备库 192.168.1.30)
pg_basebackup -h 192.168.1.30 -U repl -D /var/lib/postgresql/18/data \
              -Fp -Xs -P -R --slot=leaf_b_slot
ini
# 6. 叶子备库配置
primary_conninfo = 'host=192.168.1.30 port=5432 user=repl application_name=leaf-b'
primary_slot_name = 'leaf_b_slot'

示例二:在中间备库上同时观察接收与发送 ​

这是验证级联是否真正建立的关键检查。

sql
-- 接收侧
SELECT status, flushed_lsn, slot_name, sender_host
FROM pg_stat_wal_receiver;
text
  status   | flushed_lsn |  slot_name  |  sender_host
-----------+-------------+-------------+---------------
 streaming | 0/3020000   | relay_slot  | 192.168.1.10
(1 row)
sql
-- 发送侧
SELECT application_name, state, flush_lsn, replay_lsn,
       pg_wal_lsn_diff(pg_current_wal_lsn(), flush_lsn) AS downstream_lag
FROM pg_stat_replication;
text
 application_name |   state   | flush_lsn | replay_lsn | downstream_lag
------------------+-----------+-----------+------------+----------------
 leaf-b           | streaming | 0/3020000 | 0/301F800  |              0
 leaf-c           | streaming | 0/301C000 | 0/301B000  |          16384
(2 rows)

注意 pg_current_wal_lsn() 在中间备库上返回的是它自己的接收位置(因为备库也在写 pg_wal),因此这个查询在备库上同样可用。

示例三:识别级联中的延迟来源 ​

假设叶子备库报告延迟偏大,需要判断是「中间备库发得慢」还是「链路或叶子慢」。

sql
-- 在中间备库上:看下游的接收进度
SELECT application_name, state,
       pg_wal_lsn_diff(sent_lsn, flush_lsn) AS in_transit
FROM pg_stat_replication;
text
 application_name |   state   | in_transit
------------------+-----------+------------
 leaf-b           | streaming |          0
 leaf-c           | streaming |      65536
(2 rows)
sql
-- 在叶子备库 C 上:看自己的接收与重放
SELECT pg_last_wal_receive_lsn() AS receive_lsn,
       pg_last_wal_replay_lsn()  AS replay_lsn,
       pg_wal_lsn_diff(pg_last_wal_receive_lsn(),
                       pg_last_wal_replay_lsn()) AS not_replayed;
text
 receive_lsn | replay_lsn | not_replayed
-------------+------------+--------------
 0/301B000   | 0/301A800  |         2048
(1 row)

判读方式:

  • 若中间备库的 in_transit 很大,说明链路或叶子接收慢;
  • 若叶子备库的 not_replayed 很大,说明叶子重放慢;
  • 若两者都小但叶子仍报告时间延迟大,多半只是「没有新事务」造成的错觉。

示例四:检查级联中的复制槽堆积 ​

sql
-- 在中间备库上:检查给下游用的槽
SELECT slot_name, active, wal_status,
       pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) AS lag
FROM pg_replication_slots
ORDER BY slot_name;
text
   slot_name  | active | wal_status |   lag
--------------+--------+------------+---------
 leaf_b_slot  | t      | reserved   | 4096 bytes
 leaf_c_slot  | f      | unreserved | 512 MB
 relay_slot   | t      | reserved   | 8192 bytes
(3 rows)

leaf_c_slot 已不活跃且落后 512MB,处于 unreserved 状态。这正是「中间备库磁盘被下游拖累」的典型信号:需要尽快恢复叶子备库 C 的连接,或评估是否让该槽失效(放弃 C 并重建)。

示例五:模拟中间备库短暂中断 ​

理解「延迟显现」现象的一个方法是在测试环境主动中断中间备库到主库的链路,然后观察叶子备库的表现。

sql
-- 叶子备库上持续观察接收位置
SELECT now() AS t,
       pg_last_wal_receive_lsn() AS receive_lsn,
       pg_wal_lsn_diff(pg_last_wal_receive_lsn(),
                       pg_last_wal_replay_lsn()) AS lag;
text
             t              | receive_lsn | lag
----------------------------+-------------+-----
 2026-09-30 10:10:00+08     | 0/3050000   |   0
 2026-09-30 10:10:30+08     | 0/3052000   |   0
 2026-09-30 10:11:00+08     | 0/3052000   |   0
(3 rows)

中断发生后,叶子备库仍能在一段时间内接收中间备库本地已有的 WAL(receive_lsn 继续前进),直到中间备库的本地 WAL 发完才停滞。这段「看起来还正常」的窗口长度取决于中间备库本地积压的 WAL 量,因此不能靠叶子备库的状态来判断主库与中间备库之间的链路健康——必须在中间备库上直接监控。

注意事项 ​

中间备库必须同时具备「接收」和「发送」两类参数。 只配了 primary_conninfo 而没配 wal_level / max_wal_senders 时,它无法为下游提供服务。这一点与「普通备库」的配置清单不同。

每对上下游关系都需要自己的复制槽。 槽位于上游节点本地。级联拓扑中槽的数量等于边数,主库上的槽只管第一层。不要以为在主库建了一个槽就能覆盖整棵树。

同步复制不能跨越级联。 只有直连主库的备库才能被 synchronous_standby_names 声明为同步备库。叶子备库对主库不可见,无法参与同步确认。

级联的故障影响面是「子树」。 中间备库故障会让其下所有叶子备库的复制中断。因此中间层需要更高的可用性要求与更严格的监控。

延迟会沿链路叠加。 每增加一层就多一段传播延迟。对延迟敏感的场景应避免级联,或用「单层 + 区域汇聚」的方式把深度控制在最小。

中间备库的 pg_wal 会比普通备库大。 它要同时为「自己重放」和「下游落后」保留 WAL,磁盘规划要把这部分算进去,并配置 max_slot_wal_keep_size。

主库到中间备库的链路中断会「延迟显现」。 由于中间备库本地有 WAL 缓存,叶子节点会在一段时间内继续正常接收,导致故障被滞后发现。必须直接在中间备库上监控其接收状态。

提升中间备库后,下游配置通常无需修改。 下游连接的上游没变,这是级联在切换场景下的一个优势。但仍需确认叶子节点的延迟在可接受范围,并注意时间线已递增。

不要在中间备库上禁用 hot_standby。 虽然级联的转发不依赖重放,但禁用热备会失去只读能力,且不符合备库的常规用途。

层数不要贪多。 PostgreSQL 没有层数限制,但实践上两层已经足够覆盖绝大多数分支场景。更深的拓扑应当考虑替代方案。

最佳实践 ​

  1. 只在收益明确时使用级联。 备库少于 3 台、或网络资源充足时,直连主库更简单可靠。级联的价值在于「把源节点的负担与备库数量解耦」。

  2. 级联深度控制在两层以内。 主库 → 区域汇聚 → 叶子,已经能实现绝大部分收益。更深的层次会让延迟与排查复杂度不可控。

  3. 把汇聚节点放在靠近叶子的一侧。 让最昂贵的跨地域链路只走一次,用它覆盖尽可能多的下游节点,这是级联的核心收益来源。

  4. 给中间备库配置更高规格的硬件与更严格的监控。 它是子树单点,故障影响面大,值得比叶子备库更多的资源投入。

  5. 中间备库必须配置 wal_level = replica、max_wal_senders、max_replication_slots。 这三项是「发送方」能力的基础,缺一不可。

  6. 每一对上下游都使用独立复制槽,并设置上限。 主库与中间备库上都要配置 max_slot_wal_keep_size,避免下游掉线拖垮上游磁盘。

  7. 在 PostgreSQL 18 上评估 idle_replication_slot_timeout。 它能自动失效长期不活跃的叶子槽,是级联场景下防止 WAL 无限堆积的有效手段。

  8. 在每个节点上都监控「接收侧」与「发送侧」两组指标。 只看叶子节点的状态会漏掉上游链路故障,因为故障的显现可能被本地 WAL 缓存掩盖。

  9. 明确同步复制只能作用于第一层。 如果业务要求 RPO = 0,必须有一台直连主库的备库承担同步角色,不能指望叶子节点。

  10. 为中间备库单独规划磁盘容量。 其 pg_wal 需要容纳「自身接收积压 + 所有下游的落后量」,规划时按最坏情况估算。

  11. 把级联拓扑画进运维文档。 记录每个节点的上游、槽名、application_name。故障时能立刻知道「该去哪个节点上查哪个槽」,显著缩短恢复时间。

  12. 定期演练「中间备库故障」场景。 这是级联最危险的故障类型,应当验证:下游能否快速切换上游、旧中间节点的僵尸槽如何清理、数据是否会丢失。

下一节

继续学习:延迟备库

参考链接 ​