级联复制
级联复制(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 发给下游备库。
┌──────────────────────────────────────┐
│ 主库 (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 也有若干行(它在发送)。
级联的延迟模型
级联引入的额外延迟来自「每一跳都要完整走一遍接收 + 落盘 + 再发送」。总延迟近似为各跳延迟之和:
总延迟 ≈ 主库→第一层延迟 + 第一层→第二层延迟 + ... + 最后一跳延迟每一跳的延迟又由该段的网络往返、落盘耗时、以及中间节点的负载共同决定。因此:
- 两层的级联,延迟大约是直连的两倍;
- 跨地域级联(例如主库在 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 里填的上游不是主库,而是中间备库。
第一步,中间备库按普通备库方式搭建(连接主库):
# 在中间备库上:正常连主库
pg_basebackup -h 192.168.1.10 -U repl -D /var/lib/postgresql/18/data \
-Fp -Xs -P -R --slot=relay_slot# 中间备库的 postgresql.auto.conf
primary_conninfo = 'host=192.168.1.10 port=5432 user=repl application_name=relay-a'
primary_slot_name = 'relay_slot'第二步,让中间备库具备「发送能力」。这些参数必须设置,否则它无法为下游提供 WAL:
# 中间备库上的 postgresql.conf
wal_level = replica # 至少要 replica,才能向下游发送
max_wal_senders = 10 # 中间节点的发送名额,容纳其下游数量
max_replication_slots = 10 # 中间节点自己的槽上限
hot_standby = on # 保留只读能力第三步,在中间备库上创建给下游用的复制槽:
-- 在中间备库上执行(注意:不是主库)
SELECT pg_create_physical_replication_slot('leaf_b_slot');
SELECT pg_create_physical_replication_slot('leaf_c_slot');第四步,放行下游的复制连接(中间备库上的 pg_hba.conf):
# 中间备库的 pg_hba.conf:允许下游网段做复制
host replication repl 192.168.2.0/24 scram-sha-256第五步,叶子备库从中间备库拉取基础备份,把 primary_conninfo 指向中间备库:
# 在叶子备库 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# 叶子备库的 postgresql.auto.conf
primary_conninfo = 'host=192.168.1.30 port=5432 user=repl application_name=leaf-b'
primary_slot_name = 'leaf_b_slot'验证级联是否生效(在中间备库上):
-- 中间备库:既有接收(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; 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 |
| 中间备库 → 叶子备库 B | 5 ms |
| 中间备库 → 叶子备库 C | 80 ms(跨地域) |
那么:
- 中间备库的重放延迟约在 20 ms 量级(外加落盘与重放耗时);
- 叶子备库 B 约 25 ms 量级;
- 叶子备库 C 约 100 ms 量级。
这解释了为什么跨地域级联要把「汇聚节点」放在靠近叶子的一侧,而不是靠近主库一侧——把最贵的跨地域链路只走一次,用它去覆盖更大范围的叶子节点,是级联的核心收益。
实际延迟还受「批量发送」影响:WAL 通常不是逐条发送,而是按批次推送。因此小事务密集的场景下,单跳的实际感知延迟可能明显高于理论链路延迟。
复制槽与磁盘的连锁风险
级联拓扑下,复制槽的磁盘风险会被放大,因为存在「级联失效」的可能:
- 叶子备库 C 长期掉线;
- 中间备库的
leaf_c_slot不断保留 WAL; - 中间备库的
pg_wal持续增长; - 中间备库磁盘写满或被迫截断 WAL;
- 中间备库既无法重放,也无法为其他下游提供 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 发完,所有叶子会同时停滞。这个「延迟显现」的特性会让故障被滞后发现,因此对中间备库与主库之间链路的监控必须敏感。
提升中间备库后的拓扑处理
如果中间备库被提升为新主库(例如主库永久故障),拓扑会变成:
中间备库(提升后成为新主库)
│
┌───────┴───────┐
▼ ▼
叶子备库 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 继续转发下去。
几个实践要点:
- 提升中间备库是级联拓扑里最危险的操作。 一旦中间节点被提升,它下面挂着的所有叶子节点都必须把
primary_conninfo改指向它,并可能需要重建;如果只是想让中间节点重新跟随原主库,则必须用pg_rewind或重建来处理时间线分叉。 - 叶子节点的
recovery_target_timeline应当保持默认的'latest',这样上游切换时间线后它能自动跟上,不需要人工干预。 - 归档(如果启用了)要按时间线分别保存。
archive_command产生的归档文件名包含时间线 ID,混合存放在同一目录是可行的,但归档清理策略(pg_archivecleanup)必须明确以哪条时间线为准。 - 故障切换后要复核每个节点的
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 上,而不是主库上。
这带来两个实际影响:
- 同步复制保证的是「主库 → 中间备库」这一跳的持久性,中间备库之后再转发给叶子节点的过程仍然是异步的;因此「同步」并不等于「数据已经在所有叶子节点上落地」。
- 如果想要更强的保证,可以考虑在中间备库上也配置同步复制,让它把「叶子节点已收到」作为自己向主库反馈的条件之一——但这会进一步放大延迟,实践中很少这样做。
选择级联 + 同步复制组合时,应当明确「同步这个词覆盖到拓扑的哪一段」,避免在故障切换时才发现预期与实际不符。本系列后续章节会详细展开同步复制的配置方式。
实战示例
示例一:搭建两层级联拓扑
完整走一遍「主库 → 中间备库 → 两个叶子备库」的搭建。
# 1. 主库上创建中间备库的槽
psql -h 192.168.1.10 -U postgres \
-c "SELECT pg_create_physical_replication_slot('relay_slot');"# 2. 中间备库从主库拉基础备份
pg_basebackup -h 192.168.1.10 -U repl -D /var/lib/postgresql/18/data \
-Fp -Xs -P -R --slot=relay_slot# 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-- 4. 中间备库上创建下游槽
SELECT pg_create_physical_replication_slot('leaf_b_slot');
SELECT pg_create_physical_replication_slot('leaf_c_slot');# 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# 6. 叶子备库配置
primary_conninfo = 'host=192.168.1.30 port=5432 user=repl application_name=leaf-b'
primary_slot_name = 'leaf_b_slot'示例二:在中间备库上同时观察接收与发送
这是验证级联是否真正建立的关键检查。
-- 接收侧
SELECT status, flushed_lsn, slot_name, sender_host
FROM pg_stat_wal_receiver; status | flushed_lsn | slot_name | sender_host
-----------+-------------+-------------+---------------
streaming | 0/3020000 | relay_slot | 192.168.1.10
(1 row)-- 发送侧
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; 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),因此这个查询在备库上同样可用。
示例三:识别级联中的延迟来源
假设叶子备库报告延迟偏大,需要判断是「中间备库发得慢」还是「链路或叶子慢」。
-- 在中间备库上:看下游的接收进度
SELECT application_name, state,
pg_wal_lsn_diff(sent_lsn, flush_lsn) AS in_transit
FROM pg_stat_replication; application_name | state | in_transit
------------------+-----------+------------
leaf-b | streaming | 0
leaf-c | streaming | 65536
(2 rows)-- 在叶子备库 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; receive_lsn | replay_lsn | not_replayed
-------------+------------+--------------
0/301B000 | 0/301A800 | 2048
(1 row)判读方式:
- 若中间备库的
in_transit很大,说明链路或叶子接收慢; - 若叶子备库的
not_replayed很大,说明叶子重放慢; - 若两者都小但叶子仍报告时间延迟大,多半只是「没有新事务」造成的错觉。
示例四:检查级联中的复制槽堆积
-- 在中间备库上:检查给下游用的槽
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; 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 并重建)。
示例五:模拟中间备库短暂中断
理解「延迟显现」现象的一个方法是在测试环境主动中断中间备库到主库的链路,然后观察叶子备库的表现。
-- 叶子备库上持续观察接收位置
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; 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 没有层数限制,但实践上两层已经足够覆盖绝大多数分支场景。更深的拓扑应当考虑替代方案。
最佳实践
只在收益明确时使用级联。 备库少于 3 台、或网络资源充足时,直连主库更简单可靠。级联的价值在于「把源节点的负担与备库数量解耦」。
级联深度控制在两层以内。 主库 → 区域汇聚 → 叶子,已经能实现绝大部分收益。更深的层次会让延迟与排查复杂度不可控。
把汇聚节点放在靠近叶子的一侧。 让最昂贵的跨地域链路只走一次,用它覆盖尽可能多的下游节点,这是级联的核心收益来源。
给中间备库配置更高规格的硬件与更严格的监控。 它是子树单点,故障影响面大,值得比叶子备库更多的资源投入。
中间备库必须配置
wal_level = replica、max_wal_senders、max_replication_slots。 这三项是「发送方」能力的基础,缺一不可。每一对上下游都使用独立复制槽,并设置上限。 主库与中间备库上都要配置
max_slot_wal_keep_size,避免下游掉线拖垮上游磁盘。在 PostgreSQL 18 上评估
idle_replication_slot_timeout。 它能自动失效长期不活跃的叶子槽,是级联场景下防止 WAL 无限堆积的有效手段。在每个节点上都监控「接收侧」与「发送侧」两组指标。 只看叶子节点的状态会漏掉上游链路故障,因为故障的显现可能被本地 WAL 缓存掩盖。
明确同步复制只能作用于第一层。 如果业务要求 RPO = 0,必须有一台直连主库的备库承担同步角色,不能指望叶子节点。
为中间备库单独规划磁盘容量。 其
pg_wal需要容纳「自身接收积压 + 所有下游的落后量」,规划时按最坏情况估算。把级联拓扑画进运维文档。 记录每个节点的上游、槽名、
application_name。故障时能立刻知道「该去哪个节点上查哪个槽」,显著缩短恢复时间。定期演练「中间备库故障」场景。 这是级联最危险的故障类型,应当验证:下游能否快速切换上游、旧中间节点的僵尸槽如何清理、数据是否会丢失。
下一节
继续学习:延迟备库
参考链接
- PostgreSQL 官方文档:Log-Shipping Standby Servers — Cascading Replication
- PostgreSQL 官方文档:Replication — Configuration Parameters
- PostgreSQL 官方文档:Monitoring Database Activity — Cumulative Statistics System
- PostgreSQL 官方文档:Streaming Replication Protocol
- PostgreSQL 官方文档:Hot Standby
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:The pg_hba.conf File
- PostgreSQL 官方文档:System Administration Functions — Recovery Control Functions
- PostgreSQL 官方文档:High Availability, Load Balancing, and Replication