PostgreSQL 故障切换后备份为何失效:pgBackRest、archive_mode 与时间线缺口

2026-09-23 31 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:10 分钟

一次 PostgreSQL 提升操作可能只需几秒,但它会改变备份系统赖以恢复数据的时间线。一个典型陷阱是:备用节点以 archive_mode=off 启动,随后被提升为新主库;业务已经恢复,却无法在不重启数据库的情况下启用 WAL 归档。此时,即使尝试从下游备用库执行 pgBackRest 备份,甚至关闭 archive-mode-check,依然可能收到 archive_mode must be enabled

问题不只是“能否生成一个备份目录”,而是备份、WAL 和 PostgreSQL 时间线是否共同构成了一条连续、可验证的恢复路径。

为什么提升成功不等于恢复链完整

考虑一组级联复制节点:

pg1(主库) → pg2(备用库) → pg3(下游备用库)

初始主库 pg1 位于时间线 1,并正常把 WAL 推入 pgBackRest 仓库。故障发生后,团队停止 pg1 并提升 pg2:

SELECT pg_promote();
SELECT pg_switch_wal();

提升会创建新的 PostgreSQL 时间线。例如,仓库中可能出现下面的边界:

时间线 1 最后一个归档 WAL:000000010000000000000091
时间线 2 第一个归档 WAL:000000020000000000000093

这里不只是少了一个看起来连续的文件名。恢复系统还需要知道新时间线从旧时间线的哪个 LSN 分叉,这一信息记录在类似 00000002.history 的文件中:

1    0/920000A0    no recovery target specified

如果 pg2 被提升时 archive_mode=off,它不会归档切换附近的 WAL 和时间线历史文件。后来再从 pg3 开始归档,只能收集“从现在开始”的 WAL,无法补回提升时已经丢失的历史。

因此会出现两种结论完全不同的测试:

  • 恢复提升之后新做的完整备份,可能成功;
  • 从提升之前的旧备份恢复,并跨越时间线切换追到最新状态,可能失败。

前者只能证明某个新备份具备局部可恢复性,不能证明整个灾备恢复链连续。

--no-archive-mode-check 并不是关闭所有保护

在备用库备份场景中运行下面的命令:

pgbackrest --stanza=demo --no-archive-mode-check backup

仍可能得到:

ERROR: [087]: archive_mode must be enabled

这不是参数失效。archive-mode-check 处理的是另一类风险:当备用库设置 archive_mode=always 时,主库和备用库可能把逻辑上对应同一 WAL 的文件同时推入仓库,但文件校验和不一定相同。禁用该检查时,操作者必须确保只有一个归档者向目标仓库写入。

它并不会绕过 pgBackRest 对当前主库的基本要求。pgBackRest还会确认:

  • 主库启用了 archive_mode
  • archive_command 配置了 pgBackRest 的 archive-push
  • 备份一致性所需的 WAL 已经进入归档仓库。

这些检查不是多余的语法验证,而是恢复协议的一部分。修改源码移除检查,确实可能让备份命令完成,但也意味着团队必须自行证明所有 WAL、时间线历史和失败场景都得到了正确处理。

推荐配置:所有可提升节点提前启用归档

最稳妥的做法是在节点成为提升候选者之前设置 archive_mode=on。这个参数需要重启 PostgreSQL,不能等到故障切换后才处理。

可在每个可能被提升的节点上采用下面的配置,再按环境修改 stanza 名称和命令路径:

# postgresql.conf
archive_mode = on
archive_command = 'pgbackrest --stanza=demo archive-push %p'
restore_command = 'pgbackrest --stanza=demo archive-get %f "%p"'

修改 archive_mode 后需要重启:

sudo systemctl restart postgresql-18.service

如果只是修改 archive_command,重新加载配置即可:

sudo systemctl reload postgresql-18.service

若归档仓库尚未准备好,可以在集群建立之初先设置 archive_mode=on,暂时留空 archive_command,或者在明确接受“当前 WAL 不会被保存”的前提下使用占位命令:

archive_mode = on
archive_command = '/bin/true'

/bin/true 只适合作为部署阶段的占位符,它会直接丢弃待归档 WAL。节点进入生产、成为提升候选者或纳入灾备承诺之前,必须换成真正的 archive-push,执行 reload,并验证 WAL 已经到达仓库。

从备用库读取数据,仍要求主库正确归档

pgBackRest 可以从备用库读取大部分数据文件,以降低主库 I/O 压力。假设命令在 pg3 上运行,而 pg2 是当前主库,可以这样实践:

# /etc/pgbackrest.conf
[global]
repo1-path=/shared
repo1-retention-full=4
compress-type=zst
start-fast=y
backup-standby=y
log-level-console=info

[demo]
# 本机 pg3:用于读取备份数据
pg1-path=/var/lib/pgsql/18/data

# 当前主库 pg2:用于协调备份开始、结束及 WAL 检查
pg2-host=pg2
pg2-host-user=postgres
pg2-path=/var/lib/pgsql/18/data

先验证远程访问和仓库状态:

ssh postgres@pg2 'psql -Atqc "SELECT pg_is_in_recovery()"'
pgbackrest --stanza=demo check
pgbackrest --stanza=demo info

确认 pg2 返回 f、pg3 返回 t 后,再执行备份:

pgbackrest --stanza=demo --type=full backup

正常日志应体现三个关键动作:

wait for replay on the standby to reach ...
check archive for prior segment ...
check archive for segment(s) ...

这说明 pgBackRest 等待备用库回放到备份起点,并确认备份一致性所需 WAL 已归档。但要注意:backup-standby=y 只改变数据文件主要从哪里读取,并没有把主库的归档责任转交给备用库。

切换前可以直接执行的预检

可以在候选节点上运行下面的脚本。执行前把 STANZA 和 PostgreSQL 服务名改成实际值:

#!/usr/bin/env bash
set -euo pipefail

STANZA="demo"

echo "== PostgreSQL role and archive settings =="
psql -X -v ON_ERROR_STOP=1 -At <<'SQL'
SELECT 'in_recovery=' || pg_is_in_recovery();
SELECT 'archive_mode=' || current_setting('archive_mode');
SELECT 'archive_command=' || current_setting('archive_command');
SQL

echo "== pgBackRest configuration and repository =="
pgbackrest --stanza="$STANZA" check
pgbackrest --stanza="$STANZA" info

echo "== Archiver statistics =="
psql -X -v ON_ERROR_STOP=1 -x -c \
  "SELECT archived_count, failed_count, last_archived_wal,
          last_archived_time, last_failed_wal, last_failed_time
     FROM pg_stat_archiver;"

仅看到 archive_mode=on 还不够。预检至少应确认:

  1. archive_command 指向正确的 stanza 和仓库;
  2. failed_count 没有持续增长;
  3. 最近切换出的 WAL 能在 pgBackRest 仓库中找到;
  4. 复制延迟处于可接受范围;
  5. 候选节点的 SSH/TLS、仓库权限和凭据可用。

切换后可主动生成一个 WAL 边界并再次检查:

psql -X -v ON_ERROR_STOP=1 -c 'SELECT pg_switch_wal();'
pgbackrest --stanza=demo check
pgbackrest --stanza=demo info

验收标准不能停在“backup completed”

一次完整的灾备演练至少要覆盖两条路径:

  • 恢复故障切换之后创建的新备份;
  • 恢复故障切换之前的备份,并使用归档 WAL 跨越时间线分叉。

还应检查仓库中是否包含对应的 .history 文件,并验证恢复目标能够到达预期时间点或 LSN。只做一次最新全量备份的 restore,无法发现旧备份与新时间线之间的断层。

如果生产环境已经出现“新主库 archive_mode=off 且不希望重启”的局面,建议优先选择受支持的修复方式:安排重启,或者把业务受控切换到一个已经正确启用归档的备用节点。不要把修改 pgBackRest 源码当成常规运行方案。

最终应遵循一条简单规则:所有可能被提升的 PostgreSQL 节点,都应在故障发生之前启用 archive_mode=on,配置并验证 archive_command 备份命令成功只是起点;能够跨备份、WAL 和时间线完成恢复,才是灾备真正成立的证据。


相关推荐