数据库基础体系 · 第 24/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

PostgreSQL 复制与高可用:流复制、复制槽、PITR 和故障切换

在 PostgreSQL 中,“复制”解决的是把一个集群产生的数据变化传递到另一个集群;“高可用”则进一步解决主库故障后如何判断、切换、恢复和重新加入。两者相关,但不是同一个问题:

  • 流复制负责传输和重放 WAL。
  • 复制槽负责让主库保留某个副本尚未消费的 WAL。
  • PITR(Point-in-Time Recovery,时间点恢复)负责利用基础备份和归档 WAL 恢复到某个历史时刻。
  • 故障切换负责在主库不可用时选择一个副本提升为新主库,并避免双主写入。

理解这些机制的共同基础是 WAL、LSN、时间线和 PostgreSQL 的进程模型。

本文示例以 PostgreSQL 16 的 Linux 部署为边界,假设:

  • 主库地址为 10.0.0.10
  • 备库地址为 10.0.0.11
  • 数据目录分别为 /var/lib/postgresql/16/main
  • PostgreSQL 服务账户为 postgres
  • 示例使用物理流复制,不讨论逻辑复制的表级复制语义。

不同发行版的数据目录、服务名和配置文件路径可能不同,但 SQL 视图和复制协议的核心语义不因此改变。


一、先建立共同模型:WAL、LSN 与复制状态

1. WAL 是什么

PostgreSQL 不会在每次更新一行时先把所有数据页直接写入最终位置,再认为事务完成。它首先生成 WAL(Write-Ahead Log,预写式日志),记录足以重做数据页变化的信息。

“预写”要求满足:

WAL 持久化完成对应数据页持久化\text{WAL 持久化完成} \prec \text{对应数据页持久化}

这里的 \prec 表示“先于”。如果事务提交时对应 WAL 已经持久化,即使数据库进程随后崩溃,也可以通过重放 WAL 恢复数据页。

WAL 同时承担三项职责:

  1. 崩溃恢复:本机从最近的检查点开始重做 WAL。
  2. 物理流复制:备库接收主库产生的 WAL,并按顺序重放。
  3. PITR:从基础备份开始,连续重放归档 WAL,恢复到某个时间点。

因此,物理复制和 PITR 不是两套完全无关的技术,它们都依赖 WAL;区别在于 WAL 的传输方式、保存位置和恢复目标不同。

2. LSN 是什么

LSN(Log Sequence Number)是 WAL 日志流中的位置。它通常写成:

16/B374D848

可以把 LSN 抽象为 WAL 字节流中的单调位置。假设:

  • L1 是备库已经重放的位置;
  • L2 是主库已经写入的位置;

则当 L2 > L1 时,说明主库存在备库尚未重放的 WAL。

常见 LSN 含义如下:

  • write_lsn:WAL 已写入接收方内核或文件缓冲的进度;
  • flush_lsn:WAL 已刷新到持久存储的进度;
  • replay_lsn:备库已经重放到的数据变化位置;
  • restart_lsn:复制槽要求主库至少保留 WAL 的起点。

LSN 差值可以估算物理 WAL 字节滞后:

SELECT
    client_addr,
    state,
    pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), replay_lsn))
        AS replay_lag_bytes,
    write_lag,
    flush_lag,
    replay_lag
FROM pg_stat_replication;

这段查询在主库执行。pg_wal_lsn_diff(a, b) 返回从 ba 的 WAL 字节数。它描述的是 WAL 位置差异,不等于必然的业务时间延迟:少量 WAL 可能对应大量计算,反之亦然。


二、物理流复制的完整数据流

1. 参与复制的进程

一个典型的物理流复制链路包含:

主库客户端
   │
   ▼
主库 backend
   │ 生成 WAL
   ▼
WAL 写入与持久化
   │
   ▼
主库 walsender
   │ 复制协议
   ▼
备库 walreceiver
   │ 写入 pg_wal
   ▼
备库 startup/recovery 进程
   │ 重放 WAL
   ▼
备库数据页与查询快照

主库上的 walsender 不是单独写数据的服务,它为某个复制连接读取 WAL,并根据备库反馈发送数据。

备库上的 walreceiver 接收 WAL;备库的恢复进程负责按顺序重放 WAL。启用 hot_standby 后,备库可以在恢复期间接受只读查询,但查询看到的是备库已经重放完成的状态。

2. 流复制复制什么

物理流复制复制的是集群级别的 WAL,因此复制的是数据库集群的物理状态,而不是某些表上的 SQL 操作。它通常具有这些边界:

  • 主库上的所有数据库都会复制到物理备库;
  • 表、索引、系统目录和数据库对象都按物理方式复制;
  • 备库不能独立修改同一份复制数据;
  • 备库通常用于只读查询、备份、灾备和故障切换;
  • 主库和备库需要使用兼容的 PostgreSQL 大版本,不能把 PostgreSQL 16 的物理 WAL 直接当作 PostgreSQL 15 的数据输入。

物理复制和逻辑复制的核心区别是:逻辑复制传递行级变化及其关系映射,物理复制传递 WAL 和页面级恢复信息。题目中的流复制默认指物理流复制。


三、配置一个可运行的物理流复制集群

1. 主库配置

在主库 10.0.0.10postgresql.conf 中设置:

listen_addresses = '*'

wal_level = replica
max_wal_senders = 10
max_replication_slots = 10

# 允许备库在恢复期间接受只读查询
hot_standby = on

这些参数含义是:

  • wal_level = replica:生成支持物理复制和归档恢复所需级别的 WAL;
  • max_wal_senders:允许同时建立多少个 WAL 发送进程;
  • max_replication_slots:允许创建多少个复制槽;
  • hot_standby:备库恢复期间允许只读连接。

wal_levelmax_wal_sendersmax_replication_slots 通常需要重启才能生效;具体参数是否可 reload,应以当前版本的参数上下文为准。

pg_hba.conf 中允许复制用户从备库连接:

host    replication    repl    10.0.0.11/32    scram-sha-256

创建复制用户:

CREATE ROLE repl
  WITH LOGIN REPLICATION PASSWORD 'replace-with-a-strong-password';

这里的 REPLICATION 属性允许该角色建立复制连接,不等同于超级用户。生产环境应使用专门账户、强密码,并限制来源地址。

修改 pg_hba.conf 后重新加载:

SELECT pg_reload_conf();

确认配置:

SELECT name, setting, context
FROM pg_settings
WHERE name IN (
    'wal_level',
    'max_wal_senders',
    'max_replication_slots',
    'hot_standby'
);

2. 用复制槽创建备库

复制槽不是备库本身,它是主库上的一条持久化消费状态记录。先在主库创建一个物理复制槽:

SELECT pg_create_physical_replication_slot('standby01');

预期结果类似:

 slot_name | lsn
-----------+-----
 standby01 |

新建槽还没有消费位置;当备库使用该槽连接并反馈进度后,主库会据此计算保留边界。

在备库上停止 PostgreSQL,然后从主库执行基础备份:

sudo -u postgres pg_basebackup \
  -h 10.0.0.10 \
  -U repl \
  -D /var/lib/postgresql/16/main \
  -Fp \
  -Xs \
  -P \
  -R \
  -S standby01

各选项的作用:

  • -D:目标数据目录;
  • -Fp:普通文件格式;
  • -Xs:在备份过程中通过复制协议同时传输 WAL,避免基础备份与 WAL 不匹配;
  • -P:显示进度;
  • -R:在备库数据目录中生成 standby.signal,并写入连接参数;
  • -S standby01:使用名为 standby01 的复制槽。

pg_basebackup 的输入是主库当前一致的基础备份,输出是可启动的数据库目录。它不能把正在运行的任意文件目录简单复制成一致备份;直接使用 cp 复制活动数据目录可能得到不可恢复或不一致的结果。

检查 -R 写入的配置:

sudo -u postgres cat /var/lib/postgresql/16/main/postgresql.auto.conf

通常会看到类似:

primary_conninfo = 'user=repl ... host=10.0.0.10 ...'
primary_slot_name = 'standby01'

并且数据目录下存在:

standby.signal

在 PostgreSQL 12 及以后,standby.signal 表示启动时进入备库持续恢复模式。旧版本使用过 recovery.conf,不能把旧版本配置方式机械套用到新版本。

启动备库:

sudo systemctl start postgresql

在主库检查发送端:

SELECT
    application_name,
    client_addr,
    state,
    sync_state,
    sent_lsn,
    write_lsn,
    flush_lsn,
    replay_lsn
FROM pg_stat_replication;

在备库检查接收端:

SELECT
    status,
    sender_host,
    sender_port,
    slot_name,
    written_lsn,
    flushed_lsn,
    latest_end_lsn
FROM pg_stat_wal_receiver;

在备库确认是否仍处于恢复状态:

SELECT pg_is_in_recovery();

预期结果:

 t

如果主库查询不到 pg_stat_replication 行,应检查:

  1. 备库是否启动;
  2. primary_conninfo 中的地址、用户和密码是否正确;
  3. 主库 pg_hba.conf 是否允许该地址;
  4. 防火墙和监听地址是否允许连接;
  5. 复制槽名称是否一致;
  6. 备库日志是否出现认证失败、WAL 缺失或时间线不匹配。

四、异步复制与同步复制

1. 异步复制的提交路径

默认情况下,物理流复制是异步的。事务提交时,主库只需要满足本地提交所需的 WAL 持久化条件,不等待备库确认。

数据流可以表示为:

主库生成 WAL
  → 主库 WAL 持久化
  → 向客户端返回 COMMIT
  → 发送给备库
  → 备库接收并持久化
  → 备库重放

因此,主库故障时可能出现:

客户端已经收到提交成功
但该事务的 WAL 尚未到达备库

这就是异步复制可能产生非零 RPO(Recovery Point Objective,恢复点目标)的原因。

2. 同步复制的提交路径

同步复制要求主库等待指定备库确认。主库配置例如:

synchronous_standby_names = 'FIRST 1 (standby01)'

备库发送的 application_name 必须与 standby01 对应。也可以使用 ANY 表示从候选备库中满足数量要求的任意节点。

事务会等待到什么程度,还受 synchronous_commit 影响:

SET synchronous_commit = on;

常见语义如下:

  • remote_write:等待同步备库确认 WAL 已写入其操作系统缓存;
  • onremote_flush:等待同步备库确认 WAL 已刷新到持久存储;
  • remote_apply:等待同步备库已经重放该事务。

因此,remote_applyon 等待得更晚,但它可以让主库在提交成功后更有把握地看到同步备库已经应用该事务。

同步复制并不是“自动高可用”。它改变的是提交确认条件:

提交延迟 = 本地提交等待 + 同步备库确认等待

如果同步备库不可用,主库可能出现提交阻塞。可以通过:

synchronous_standby_names = 'ANY 1 (standby01, standby02)'

让任意一个候选备库满足确认条件,但这仍然需要正确配置、正确监控和明确的故障处理策略。

同步复制也不等于绝对零数据丢失。它至少需要同时满足:

  1. 事务确实使用了要求远端确认的 synchronous_commit
  2. 确认的备库在故障时仍可用或其存储数据未丢失;
  3. 故障切换选择的是包含该提交的备库;
  4. 没有发生双主、存储回滚或人为误操作。

五、复制槽:为什么有用,以及为什么危险

1. 没有槽时的 WAL 保留问题

主库不能无限保存 WAL。检查点、归档和复制消费者共同决定某段 WAL 是否还需要。

如果备库暂时断开,主库可能在本地清理掉备库尚未获取的旧 WAL。备库重连时会收到类似错误:

requested WAL segment ... has already been removed

此时如果没有其他来源提供缺失 WAL,备库无法继续,只能重新做基础备份,或者从归档中补齐缺失 WAL。

2. 物理复制槽的语义

物理复制槽记录某个物理消费者的 WAL 保留位置。查看槽状态:

SELECT
    slot_name,
    slot_type,
    active,
    restart_lsn,
    wal_status,
    safe_wal_size,
    xmin,
    catalog_xmin
FROM pg_replication_slots;

对物理槽而言,最重要的是:

  • active:当前是否有消费者连接;
  • restart_lsn:主库不能早于这个位置清理该槽仍需要的 WAL;
  • wal_status:槽相关 WAL 的保留状态;
  • safe_wal_size:在配置支持的版本中,对槽可能消耗的 WAL 空间提供估算;
  • xmincatalog_xmin:主要与反馈和逻辑槽相关,不能把每个字段都理解成物理 WAL 起点。

当备库使用槽并持续反馈已经接收的 WAL 后,restart_lsn 会向前推进。备库断开后,这个位置可能长时间不动。

3. 槽不是免费保险

复制槽的核心保证是:

只要槽存在且未推进,主库保留槽仍需要的 WAL\text{只要槽存在且未推进,主库保留槽仍需要的 WAL}

这同时意味着:

备库长期不消费主库 WAL 持续累积\text{备库长期不消费} \Rightarrow \text{主库 WAL 持续累积}

例如主库产生速度为 50 MB/s,备库停止 2 小时,理论上仅该备库就可能要求保留:

50×3600×2=360000 MB50 \times 3600 \times 2 = 360000\ \text{MB}

也就是约 351.6 GiB,实际还会受其他 WAL 保留因素影响。

可以设置上限:

max_slot_wal_keep_size = 64GB

超过上限后,槽不再能无限制保护 WAL;备库可能因缺少 WAL 而无法继续。这个参数本质上是在“主库磁盘耗尽”和“备库需要重建”之间做取舍,而不是消除风险。

检查槽对应的 WAL 体积:

SELECT
    slot_name,
    active,
    restart_lsn,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    ) AS retained_wal
FROM pg_replication_slots
WHERE slot_type = 'physical';

如果确认某个槽已经废弃,可以删除:

SELECT pg_drop_replication_slot('standby01');

删除前必须确认没有实例仍依赖该槽。否则主库可能很快清理掉该实例需要的 WAL。

4. 复制槽不会自动变成高可用状态

复制槽是主库本地的状态。主库提升某个备库后,原主库上的物理槽不会自动以同样方式出现在新主库上;槽元数据也不是通过普通物理 WAL 重放来实现跨主库自动迁移的高可用目录。

因此,故障切换后需要重新设计:

  • 新主库上创建对应复制槽;
  • 新备库连接到新主库并使用新槽;
  • 对逻辑槽还要额外考虑消费位置和槽元数据迁移;
  • 不能只把客户端连接地址切过去,就认为所有复制保护已经恢复。

六、复制延迟、备库查询与 Vacuum 的交互

备库能否查询,取决于 WAL 重放和 MVCC 快照之间的冲突。

例如:

  1. 备库查询打开了一个长事务;
  2. 主库执行 VACUUM,生成清理相关 WAL;
  3. 备库重放该 WAL 时,发现清理操作可能移除查询仍需要的旧版本;
  4. 备库必须在查询等待和恢复进度之间做选择。

可能出现:

canceling statement due to conflict with recovery

可以调整备库上的参数,例如:

max_standby_streaming_delay = 30s
max_standby_archive_delay = 30s

它们控制备库因查询冲突而允许恢复延迟多长时间。值越大,查询越不容易被取消,但 WAL 重放可能更滞后。

主库也可以启用:

hot_standby_feedback = on

备库会向主库反馈查询所需的 xmin,使主库避免过早清理某些旧版本。这能减少恢复冲突,但会带来新的代价:

  • 主库旧 Tuple 可能因为备库长事务而无法回收;
  • 表膨胀和磁盘增长风险增加;
  • 它不能替代合理的查询超时和长事务治理。

这与 PostgreSQL MVCC 和 Vacuum 的关系直接相关:复制不只是 WAL 网络传输,也会改变旧版本清理在主备之间的约束。


七、PITR:从基础备份恢复到指定时间点

1. PITR 的组成

PITR 至少需要:

  1. 一个一致的基础备份;
  2. 从基础备份覆盖到目标时刻的连续 WAL 归档;
  3. 恢复时能够按顺序取回这些 WAL;
  4. 一个恢复目标,例如时间点、事务 ID 或恢复目标名称。

基础备份提供某个起点状态,WAL 提供从该起点开始的变化。若基础备份完成于 LSN L0,目标时刻对应的 WAL 位置是 L1,则恢复过程必须能取得:

[L0,L1][L0, L1]

范围内连续且可读的 WAL。

缺失其中任何一个必要 WAL 段,PITR 都不能越过该缺口继续恢复。

2. 开启归档

主库配置:

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /var/lib/postgresql/wal_archive/%f && cp %p /var/lib/postgresql/wal_archive/%f'

创建目录并授权:

sudo install -d -o postgres -g postgres /var/lib/postgresql/wal_archive

archive_command 返回 0 表示归档成功。示例中的 test ! -f 用来避免覆盖同名文件,但生产环境还需要考虑归档目录的可靠存储、校验、清理、跨主机传输和故障告警。

归档命令失败时,PostgreSQL 会保留相关 WAL 并重试。归档目录或归档链路不可用时,主库的 pg_wal 可能增长,最终影响数据库运行。

可以设置:

archive_timeout = 60s

它会促使 PostgreSQL 在长时间没有填满一个 WAL 段时更快切换并归档当前段。它不能让归档命令绕过失败,也不能保证精确到 60 秒的恢复点;恢复粒度仍受实际 WAL、提交记录和恢复目标语义影响。

3. 创建基础备份

可以使用:

sudo -u postgres pg_basebackup \
  -D /var/backups/postgresql/base-20250308 \
  -Fp \
  -X fetch \
  -P

-X fetch 表示备份结束后获取所需 WAL;备份期间主库必须保留这些 WAL,通常需要有足够的 WAL 保留策略或使用复制槽。另一种方式是 -X stream,在备份期间通过额外连接流式接收 WAL。

备份完成后,可以使用:

sudo -u postgres pg_verifybackup /var/backups/postgresql/base-20250308

验证备份的校验信息和备份目录结构。验证成功不等于已经完成一次真实恢复演练,但能发现一部分备份损坏或元数据问题。

4. 恢复到指定时间点

假设要把备份恢复到:

2025-03-08 14:30:00+08

先停止一个用于恢复的 PostgreSQL 实例,将基础备份恢复到数据目录,然后创建 recovery.signal

sudo -u postgres touch /var/lib/postgresql/16/main/recovery.signal

配置:

restore_command = 'cp /var/lib/postgresql/wal_archive/%f %p'
recovery_target_time = '2025-03-08 14:30:00+08'
recovery_target_action = 'promote'

启动实例:

sudo systemctl start postgresql

恢复进程会:

  1. 读取基础备份;
  2. 从归档中按顺序获取 WAL;
  3. 重放到恢复目标;
  4. 根据目标语义停止恢复;
  5. 执行 promote,使实例成为可写数据库。

restore_command 中:

  • %f 表示所需 WAL 文件名;
  • %p 表示目标路径。

如果归档文件不存在,命令应返回非零状态,使 PostgreSQL 知道暂时没有该文件,而不是把一个错误页面误当成 WAL 文件。示例的 cp 在文件不存在时会失败。

还可以使用其他恢复目标:

recovery_target_lsn = '16/B374D848'

或:

recovery_target_name = 'before_bad_deployment'

恢复目标有边界语义。例如,时间点可能落在事务提交附近;“恢复到某一时刻”并不等价于把每个业务操作按应用层意图自动回滚。需要结合事务提交记录和目标参数语义验证最终状态。

PITR 恢复出来的实例通常会进入一条新的时间线。恢复完成后,继续生成 WAL 的主线不再是原来的唯一历史分支,而是从恢复点分叉出来的新 timeline。


八、时间线:为什么恢复和提升后不能只看 LSN

LSN 只表示某条 WAL 日志流中的位置。故障切换或 PITR 后,可能出现两条不同时间线都存在相同数值范围的 LSN:

Timeline 1: ---- A ---- B ---- C
                         \
Timeline 2:              D ---- E

这里新时间线从 B 附近分叉。单独比较 16/B374D848 这样的 LSN,不能判断两个位置是否属于同一历史分支;还必须结合 timeline。

查看时间线历史文件:

pg_waldump --timeline=1 --path=/var/lib/postgresql/16/main/pg_wal

实际诊断中也应查看数据目录中的:

00000002.history

文件。时间线文件记录分叉关系,使恢复进程知道哪些 WAL 属于当前历史。

这解释了一个常见误解:

“旧主库重启后从自己的 WAL 继续运行,就能自然成为新主库。”

不能这样做。新主库已经在提升时创建了新时间线;旧主库若继续以旧时间线接受写入,就形成分叉历史。必须先隔离旧主库,再通过 pg_rewind 或重新做基础备份把它变成新主库的备库。


九、故障切换:从发现故障到重新收敛

故障切换不是单条 SQL,而是一条包含判断、隔离、提升、接入和重建的状态转换路径。

1. 故障前的状态

正常时:

主库 A:可写,timeline 1
备库 B:恢复中,跟随 timeline 1
客户端:连接 A

B 可能处于几个不同位置:

  • 已接收但未持久化 WAL;
  • 已持久化但未重放 WAL;
  • 已重放到某个 LSN;
  • 查询可见状态落后于 WAL 接收状态。

因此,切换前需要确认的是 B 的 replay_lsn,而不是只看网络连接是否存在。

2. 判断主库确实不可用

如果只是网络分区,而主库 A 仍然可以接受写入,B 直接提升会产生双主:

客户端 1 → A 写入
客户端 2 → B 写入

A 和 B 都会产生无法自动合并的物理 WAL 历史。这个问题不能靠冲突解决,因为物理复制没有提供两个可写节点的行级合并机制。

所以故障切换必须包含 fencing(隔离或栅栏):

  • 关闭或隔离旧主库;
  • 撤销旧主库的网络写入路径;
  • 通过云平台、虚拟化平台、STONITH 等方式确保旧主库不能继续服务;
  • 再提升备库。

只要无法阻止旧主库继续对外提供写服务,自动故障切换就存在脑裂风险。

3. 提升备库

确认 B 是目标备库并已完成隔离条件后,在 B 上执行:

SELECT pg_promote(true, 60);

参数含义:

  • 第一个参数表示等待提升完成;
  • 第二个参数是等待秒数。

也可以使用:

sudo -u postgres pg_ctl \
  -D /var/lib/postgresql/16/main \
  promote

提升后检查:

SELECT pg_is_in_recovery();

预期结果:

 f

再检查:

SELECT timeline_id, redo_lsn
FROM pg_control_checkpoint();

不同版本和工具输出字段可能略有差异,判断实例是否可写最直接的检查仍是 pg_is_in_recovery() 和实际写入测试。

4. 客户端切换

客户端不能永久写死旧主库地址。常见方案包括:

  • DNS 或服务发现切换;
  • 虚拟 IP;
  • 连接池路由;
  • 代理根据主库角色路由;
  • 应用配置中心切换。

这部分不由 PostgreSQL 核心复制协议自动完成。数据库已经提升,不代表已有连接、连接池和应用会自动重新连接到新主库。

5. 旧主库重新加入

旧主库恢复后不能直接启动为主库。先停止它并隔离客户端:

sudo systemctl stop postgresql

如果满足 pg_rewind 前提,可以让旧主库回退到新主库的共同历史点。典型前提包括:

  • 两个集群来自同一数据历史;
  • 旧主库在分叉前与新主库存在共同 timeline;
  • 旧主库停机;
  • 旧主库具备 wal_log_hints = on 或数据校验和已启用;
  • 所需 WAL 或归档 WAL 仍然可取得。

在旧主库配置中预先启用:

wal_log_hints = on

该参数通常需要重启。然后在旧主库停止状态下执行:

sudo -u postgres pg_rewind \
  --target-pgdata=/var/lib/postgresql/16/main \
  --source-server="host=10.0.0.11 port=5432 user=postgres dbname=postgres"

这里的源服务器是已经提升的新主库 B。pg_rewind 会比较双方共同历史,从旧主库回退或覆盖分叉后的数据页,使其回到可以继续跟随新主库的状态。

pg_rewind 失败时,不能继续把旧主库当作备库启动;应重新执行基础备份:

sudo -u postgres pg_basebackup \
  -h 10.0.0.11 \
  -U repl \
  -D /var/lib/postgresql/16/main \
  -Fp \
  -Xs \
  -P \
  -R \
  -S standby02

新主库 B 上应创建新槽:

SELECT pg_create_physical_replication_slot('standby02');

然后让旧主库作为备库连接 B,并确认:

SELECT pg_is_in_recovery();

返回 t 后,再在新主库查看:

SELECT application_name, client_addr, state, replay_lsn
FROM pg_stat_replication;

只有当旧主库已经重新成为备库、复制链路正常、客户端只写新主库时,故障切换流程才算收敛。


十、复制、归档和 PITR 如何组合

三种能力可以组合,但职责不同:

能力 主要数据来源 主要目标 典型故障
异步流复制 主库实时 WAL 快速获得近实时备库 主库故障时丢失尚未到达备库的提交
同步流复制 主库实时 WAL 和远端确认 降低已确认事务的数据丢失风险 远端确认不可用导致提交等待
复制槽 主库本地槽状态 防止消费者需要的 WAL 被清理 消费者停机导致主库磁盘增长
WAL 归档 归档存储 保存较长历史 归档失败、链路断裂、文件损坏
PITR 基础备份 + 连续归档 WAL 恢复到历史时间点 需要的 WAL 缺失或恢复目标选错

一个常见组合是:

主库 A
 ├── 异步备库 B:快速故障切换
 ├── 同步备库 C:降低提交丢失风险
 └── WAL 归档:支持历史恢复和灾难恢复

但 B、C 和归档系统要分别监控。流复制正常不代表归档正常;归档正常也不代表备库延迟很小。


十一、诊断:从现象定位到机制

1. 主库没有复制连接

SELECT *
FROM pg_stat_replication;

如果为空,优先检查:

sudo -u postgres psql -h 10.0.0.10 -U repl -d postgres

以及:

sudo journalctl -u postgresql

常见错误包括:

  • no pg_hba.conf entry:认证规则不匹配;
  • password authentication failed:复制用户密码错误;
  • requested WAL segment has already been removed:备库需要的 WAL 已被清理;
  • replication slot ... does not exist:槽名称不正确;
  • timeline 不匹配:节点已经发生过提升或历史分叉。

2. 槽导致主库磁盘增长

SELECT
    slot_name,
    active,
    restart_lsn,
    pg_size_pretty(
        pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)
    ) AS retained_wal
FROM pg_replication_slots;

如果某槽 active = falseretained_wal 持续增长,应先确定:

  • 对应备库是否只是临时维护;
  • 是否可以恢复该备库;
  • 是否已有新的备库替代它;
  • 是否有归档可供将来重建。

不要在没有确认依赖关系时直接删除槽。删除槽可能立即释放主库空间,却使原消费者失去继续追赶所需的 WAL。

3. 备库“连接正常但数据很旧”

主库的 pg_stat_replication 可能显示网络连接仍在,但 replay_lsn 长时间不前进。此时要区分:

  • WAL 未发送;
  • WAL 已发送但未写入;
  • WAL 已写入但未 flush;
  • WAL 已 flush 但恢复进程重放慢;
  • 重放被备库长查询冲突阻塞。

查看:

-- 主库
SELECT application_name, state, sent_lsn, write_lsn, flush_lsn, replay_lsn
FROM pg_stat_replication;

-- 备库
SELECT pg_last_wal_receive_lsn(),
       pg_last_wal_replay_lsn(),
       pg_last_xact_replay_timestamp();

pg_last_xact_replay_timestamp() 是最后重放事务的时间戳。它受业务事务产生频率和主库时钟等因素影响,不应单独作为精确延迟指标。

4. 备库查询被取消

出现:

canceling statement due to conflict with recovery

说明只读查询与恢复过程产生冲突。诊断方向包括:

  • 查询是否持有长事务;
  • pg_stat_activity 中是否有长期运行查询;
  • max_standby_streaming_delay 是否过小;
  • 是否启用了 hot_standby_feedback
  • 主库是否因此产生旧版本膨胀。

不能简单地把延迟参数调得很大,因为这可能把“查询被取消”变成“备库长期落后”。


十二、边界、误解与生产取舍

1. “备库在线”不等于“可以安全切换”

备库在线只能说明连接或进程存在。安全切换还要知道:

  • 它是否在持续接收 WAL;
  • 是否已经重放到足够新的 LSN;
  • 是否与当前主库处于同一历史;
  • 切换后客户端是否能访问它;
  • 旧主库是否已被隔离。

2. “同步复制”不等于“两个节点永远一致可写”

同步复制只有一个写主库。同步备库仍然处于恢复模式,不能同时接受对同一物理集群的写入。它保证的是某些提交要等待远端确认,而不是提供多主并发写入。

3. “复制槽”不等于“备份”

复制槽只能让主库保留某个消费者需要的 WAL:

  • 它不能替代基础备份;
  • 不能保存任意历史时间点;
  • 不能防止数据被错误更新后仍然覆盖;
  • 不能替代归档和恢复演练。

误删数据如果已经被复制到备库,备库也会重放该删除。此时需要 PITR,而不是从普通流复制备库直接取回旧状态。

4. “PITR 成功”不等于“业务恢复正确”

PITR 可以把数据库恢复到某个数据库一致状态,但业务上可能仍需要判断:

  • 目标时间是否早于错误事务提交;
  • 是否恢复到了正确的时间线;
  • 应用是否在目标时刻使用了正确时区;
  • 恢复后是否需要导出部分数据,而不是直接替换生产库;
  • 恢复实例的扩展、外部文件和应用状态是否同步。

5. 高可用必须定义 RPO 和 RTO

RPO 是允许丢失的数据时间范围,RTO 是允许不可用的时间范围。

异步复制通常通过较低延迟换取可能的数据丢失:

RPO ≈ 主库故障时,备库尚未收到或重放的 WAL

同步复制降低了已确认事务的丢失概率,但会增加提交延迟和对备库可用性的依赖。

PITR 能提供历史恢复能力,但恢复到目标点通常比直接提升一个热备库耗时更长。实际方案往往同时使用:

  • 一个或多个流复制备库;
  • WAL 归档;
  • 受监控的复制槽;
  • 明确的提升和隔离流程;
  • 定期的备份恢复及故障切换演练。

核心因果关系可以归纳为:

WAL 记录变化
流复制实时传输 WAL
备库按顺序重放 WAL
复制槽阻止消费者所需 WAL 被清理
归档保存更长时间的 WAL
PITR 从基础备份重放归档 WAL 到历史目标
故障切换通过提升备库改变写入角色
时间线记录提升或 PITR 后的历史分叉

只要把这些状态、数据流和边界分开,高可用就不再只是“配置一个 standby”,而是一个从 WAL 生成、保存、传输、重放,到故障判断、角色切换和历史恢复的完整系统。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。