PostgreSQL 备份恢复策略:pg_basebackup、WAL 归档与 PITR 在企业 Java 系统中的落地

PostgreSQL 备份恢复策略:pg_basebackup、WAL 归档与 PITR 在企业 Java 系统中的落地

1. 先看一个真实场景:备份文件在,但业务回不来

凌晨两点,一个订单系统的 DBA 被电话叫醒:有人执行了一条没有 WHERE 条件的 UPDATE,把 orders 表的 status 全部改成了 CANCELED。运维的第一反应通常是"我们每天凌晨 2 点都有全量备份,恢复它就行"。可当真的开始恢复时,问题一个接一个冒出来:

  • 全量备份是 20 小时前的,恢复它意味着丢掉这 20 小时内所有新订单,业务无法接受。
  • 备份脚本只验证了"文件生成成功",没人验证过"这些文件能恢复成可用实例"。
  • 数据库开启了归档,但归档目录一周前就因为磁盘写满而停止接收新的 WAL,没人发现。
  • 恢复出来的实例没有做时间点控制,只能回到备份那一刻,不能回到误操作前 1 秒。

这个场景说明了一件事:备份的价值不在于"备份动作成功",而在于"在限定时间内、把数据恢复到限定的状态"。企业里的备份恢复,本质是在回答三个问题:能恢复到哪个时间点?恢复要花多久?恢复出来的数据对不对?

本文围绕 PostgreSQL 的三件核心工具展开:pg_basebackup 负责拿到一份可信的全量基线,WAL 归档负责记录基线之后的所有变更,PITR(Point-In-Time Recovery,时间点恢复)负责把基线加变更重放到你指定的那个瞬间。三者缺一不可,只堆其中任何一个都无法形成闭环。

2. 一句话模型与全局框架

先用一句普通中文给出核心模型:先做一份数据库的"全量照片",再持续记录照片之后的每一次改动,恢复时把照片按顺序叠加改动,直到停在你要的那一刻。 全量照片是 pg_basebackup 的产物,改动记录是归档的 WAL,叠加过程就是 PITR。

把整体拆成四个部分来看,它们的关系可以用一条链路串起来:

text 复制代码
[业务写入] --> [PostgreSQL 主库]
                   |
                   | 1. 全量物理拷贝 (pg_basebackup)
                   v
             [基础备份目录 base.tar.gz]
                   |
                   | 2. 持续归档 (archive_command)
                   v
             [WAL 归档目录 / 对象存储]
                   |
                   | 3. 恢复时重放 (recovery_target_time)
                   v
             [恢复到指定时间点的新实例] --> [校验] --> [切流]

这四个部分分别对应四类角色:主库 产生数据与 WAL;备份工具 负责在某个时刻抓取数据文件的一致性快照;归档通道 把 WAL 从主库搬运到独立存储;恢复流程把基线和解归档 WAL 组装成一个可用实例。

一次正常的恢复会按这个顺序走:先在生产之外准备一台机器,把基础备份解压放好;写好 restore_command 指向归档目录;在 postgresql.conf 或 postgresql.auto.conf 里指定恢复目标;启动实例,PostgreSQL 会先重放基础备份里自带的 WAL,再从归档里拉取后续 WAL,一路重放到目标点,最后把实例提升为可写。

这里最容易误解的是:基础备份并不是"整个数据库目录的冷拷贝"。如果直接 cp -r 数据目录,你很可能拷到一个不一致的状态,比如数据页已经改了但对应的 WAL 还没落盘。pg_basebackup 的价值就在于它通过 PostgreSQL 的复制协议,在数据库运行时抓取一个一致性的快照。

3. pg_basebackup:拿到一份可信的全量基线

3.1 它解决什么问题

pg_basebackup 是 PostgreSQL 自带的物理备份工具,它通过流复制协议从主库读取整个数据目录和必要的 WAL,生成一份可以直接用于恢复的基础备份。它不做逻辑导出,不解析 SQL,只搬运物理文件和事务日志。

它适合的场景是:你需要一份和主库二进制一致、可以配合 WAL 归档做 PITR 的基线。它不适合的场景是:你只想导出某几张表、需要跨大版本迁移、或者需要恢复到架构不同的库,那些属于 pg_dump / pg_dumpall 的领域。

备份方式上,pg_basebackup 支持两种模式:

  • plain 模式:把文件直接写到目标目录,得到的是一个和主库布局相同的可启动目录。
  • tar 模式 :把数据打包成 base.tar 和若干 pg_wal.tar,适合传输到对象存储或做异地归档。

3.2 一个完整的最小示例

目标:在本地把一台 PostgreSQL 16 主库完整备份到 /backup/base,并且带上后续 PITR 需要的 WAL。前置环境:主库运行在 5432 端口,有一个具备 REPLICATION 权限的用户 repl,密码放在 .pgpass 或环境变量里。

bash 复制代码
# 前置检查:确认主库允许复制连接,并且归档已开启
psql -h 127.0.0.1 -p 5432 -U postgres -c "SHOW wal_level;"
psql -h 127.0.0.1 -p 5432 -U postgres -c "SHOW archive_mode;"
psql -h 127.0.0.1 -p 5432 -U postgres -c "SHOW archive_command;"

# 执行物理基础备份:plain 模式,写入 /backup/base,附带 WAL
mkdir -p /backup/base
pg_basebackup \
  -h 127.0.0.1 -p 5432 -U repl \
  -D /backup/base \
  -Fp \
  -Xs \
  -P \
  -R \
  -c fast

# 查看结果
ls -lh /backup/base
cat /backup/base/backup_label
cat /backup/base/postgresql.auto.conf

参数说明(它们各自在链路中的位置):-Fp 表示 plain 格式,直接得到可启动目录;-Xs 表示把备份期间产生的 WAL 一并打包进去,这样恢复时不必依赖外部归档就能先追平到备份结束点;-P 显示进度;-R 会在目标目录生成 standby.signal 和 primary_conninfo,适合直接当流复制备库用;-c fast 触发一次快速检查点,缩短备份时间。

预期结果:/backup/base 下出现完整的 PG_VERSION、base/、global/、pg_wal/ 等目录;backup_label 记录了备份的起始 WAL 位点,这是后续 PITR 的起点。

容易改错的地方:

  • 忘记 -Xs,导致备份期间的 WAL 不在备份里,恢复时必须依赖外部归档;
  • 用 root 执行导致目标目录属主是 root,PostgreSQL 启动时报权限错误;
  • 主库 wal_level 是 minimal,这样根本无法做归档和 PITR。

3.3 生产上的取舍

plain 模式简单直接,但备份过程中目标目录不能是主库正在使用的目录;tar 模式更适合做异地传输和压缩归档。如果你的数据量在几百 GB 以上,还可以考虑 pg_basebackup 配合 --compress 或外部的 pgBackRest / Barman,它们在大规模增量备份、保留策略、对象存储集成上更成熟。

pg_basebackup 是单线程流式读取,备份时长近似和数据库大小成正比。对 TB 级库,每次全量都可能压垮网络和 IO,所以生产上更常见的是"周期性全量 + 持续 WAL 归档",而不是每次恢复都从头全量。

4. WAL 归档:把每次改动安全地搬出主库

4.1 WAL 是什么、为什么要归档

WAL(Write-Ahead Log,预写式日志)是 PostgreSQL 保证崩溃一致性和复制的基础。任何数据页修改前,对应的日志必须先落盘。WAL 本身是循环使用的:写完一个段(默认 16MB)后,如果不再需要,就会被覆盖。

要让 PITR 成为可能,就必须在 WAL 段被覆盖之前,把它们复制到主库之外的存储上,这就是归档。归档解决了两个问题:一是让备份之后的变更不再丢失,二是让恢复能沿着时间轴往前走。没有归档的备份,只能恢复到备份结束那一刻。

归档的核心配置在 postgresql.conf 中,下面给出一个可复现的最小配置:

conf 复制代码
# postgresql.conf 片段:开启 WAL 归档
wal_level = replica          # 至少要 replica,PITR 需要它
archive_mode = on            # 打开归档开关
archive_command = 'test ! -f /archive/%f && cp %p /archive/%f'
archive_timeout = 60         # 最多 60 秒强制切换一次,缩短恢复窗口

archive_command 的语义非常重要:它必须满足"文件已经存在就不要覆盖,成功返回 0,失败返回非 0"。%p 是主库上的 WAL 路径,%f 是文件名。PostgreSQL 会不断重试同一条命令,直到它返回 0,然后才允许覆盖本地 WAL。这意味着如果归档目录不可写,主库的 pg_wal 会逐渐涨满,最终导致写入失败。

4.2 一个可观测的归档脚本示例

生产环境不建议用裸 cp,因为它没有错误上报、没有校验、没有分级存储。下面是一个更贴近生产的归档脚本示例,目标是归档到一个按日期分目录的存储,并在失败时留下可排查的日志。

bash 复制代码
#!/usr/bin/env bash
# /usr/local/bin/pg_archive.sh
# 输入:%p 主库 WAL 路径,%f WAL 文件名
set -euo pipefail

WAL_PATH="$1"
WAL_NAME="$2"
ARCHIVE_ROOT="/archive/pg"
DAY_DIR="${ARCHIVE_ROOT}/$(date +%Y%m%d)"
TARGET="${DAY_DIR}/${WAL_NAME}"

mkdir -p "${DAY_DIR}"

# 幂等:已存在则直接成功退出
if [ -f "${TARGET}" ]; then
  exit 0
fi

cp "${WAL_PATH}" "${TARGET}.tmp"
# 用 sha256 保证搬运过程没有损坏
sha256sum "${TARGET}.tmp" > "${TARGET}.tmp.sha256"
mv "${TARGET}.tmp" "${TARGET}"

echo "$(date -Iseconds) archived ${WAL_NAME}" >> "${ARCHIVE_ROOT}/archive.log"
exit 0

在 postgresql.conf 中把 archive_command 指向它:

conf 复制代码
archive_command = '/usr/local/bin/pg_archive.sh %p %f'

关键步骤和结果:每次 WAL 切换时,脚本把段文件复制到以日期命名的目录里,同时生成校验文件。test ! -f 或脚本里的存在性判断保证了幂等,重复调用不会破坏已有文件。失败时脚本返回非 0,PostgreSQL 会保留本地 WAL 并持续重试。

边界和常见错误:归档目录和主库必须在不同物理盘或不同机器上,否则一盘损坏会同时带走数据文件和归档;脚本必须有超时控制,否则一次网络卡顿会让 WAL 堆积;archive_mode 从 off 改成 on 需要重启,从 on 改成 always 只需 reload,这些版本相关行为要以官方文档为准。

4.3 归档监控:不要只看"有没有报错"

归档失败往往是静默的。archive_command 失败不一定立刻让业务报错,它只是让 WAL 堆积。可以在主库上周期性检查:

sql 复制代码
-- 查看归档统计:成功数、失败数、最后一次归档的 WAL
SELECT
  archived_count,
  failed_count,
  last_archived_wal,
  last_archived_time,
  last_failed_wal,
  last_failed_time
FROM pg_stat_archiver;

如果 failed_count 持续增长,或者 last_archived_time 距离现在超过 archive_timeout 的若干倍,就要告警。还有一个更直接的信号:pg_wal 目录里累积的段数量持续上升,说明旧 WAL 一直没被归档成功释放。

5. PITR:把基线和高频变更重放到指定瞬间

5.1 一句话理解 PITR 的状态流转

可以把 PITR 理解成"在一台新机器上重演历史":先把基础备份铺开,然后让 PostgreSQL 从备份结束点开始,按顺序读取归档 WAL,一条条重放,直到重放到你指定的时间点、事务 ID 或恢复点名称,最后停下并允许写入。

整体状态流转如下:

text 复制代码
[基础备份目录] --启动--> [进入恢复模式 crash recovery]
      |
      | restore_command 拉取归档 WAL
      v
[持续重放 WAL] --到达 recovery_target_time--> [一致性停止点]
      |
      | pg_ctl promote 或 recovery_target_action 触发
      v
[实例变为可写] --> [校验业务数据]

5.2 一个完整的 PITR 恢复示例

目标:把备份 /backup/base 和归档 /archive/pg 恢复成一台新实例,恢复到 2024-06-01 10:30:00 之前的状态,模拟"误删数据前 1 分钟"。前置环境:已安装同版本 PostgreSQL 16,新实例数据目录 /var/lib/pgsql/16/data,端口 5433,避免和生产冲突。

bash 复制代码
# 1. 停止目标实例,清空数据目录
pg_ctl -D /var/lib/pgsql/16/data stop -m fast || true
rm -rf /var/lib/pgsql/16/data/*

# 2. 拷贝基础备份(plain 模式下直接复制文件)
cp -a /backup/base/. /var/lib/pgsql/16/data/
chown -R postgres:postgres /var/lib/pgsql/16/data
chmod 700 /var/lib/pgsql/16/data

# 3. 指定恢复命令和恢复目标
cat >> /var/lib/pgsql/16/data/postgresql.auto.conf <<'EOF'
restore_command = 'cp /archive/pg/20240601/%f %p'
recovery_target_time = '2024-06-01 10:30:00+08'
recovery_target_action = 'promote'
EOF

# 4. 触发恢复
sudo -u postgres pg_ctl -D /var/lib/pgsql/16/data start

# 5. 观察日志:确认重放到目标点并提升
tail -f /var/lib/pgsql/16/data/log/postgresql-*.log

预期结果:日志里会看到 restored log file "0000000100000000000000A1" from archive、recovery stopping before commit of transaction ...、database system is ready to accept connections。启动后实例可写,SELECT pg_is_in_recovery(); 返回 f。

关键步骤说明:restore_command 的作用和 archive_command 对称,负责把归档 WAL 拉回本地;recovery_target_time 决定停在哪里;recovery_target_action = 'promote' 表示到达目标后自动提升。如果只想恢复到一致性点并保持只读做校验,可以用 pause,手动检查后再 promote。

容易改错的地方:

  • 归档目录里没有备份结束点之后的 WAL,恢复会停在一个较早的位置;
  • recovery_target_time 时区写错,导致恢复点偏差几小时;
  • 用 cp /archive/pg/%f 这种不带日期目录的路径,因为归档按日期分区而找不到文件。

5.3 恢复目标怎么选

PITR 支持多种恢复目标,选择哪种取决于你要"精确回到某个瞬间"还是"回到某个逻辑检查点":

恢复目标参数 含义 典型场景 注意点
recovery_target_time 恢复到指定时间戳 误操作前 1 分钟 依赖时间准确性,事务提交时间可能存在边界模糊
recovery_target_xid 恢复到指定事务 ID 已知误操作事务号 需要先从事务日志或审计里定位 xid
recovery_target_lsn 恢复到指定 WAL 位点 精确到字节级 LSN 不易人工获取,适合工具驱动
recovery_target_name 恢复到已创建的恢复点 变更窗口前后打点 需要在正常运行时提前 pg_create_restore_point
recovery_target 恢复到最近的一致性点 只要求可用,不要求精确 得到的是备份结束点或最近可恢复点

对企业 Java 系统来说,最常见的组合是:在发布前用 pg_create_restore_point 打一个命名恢复点,配合 recovery_target_name 做发布回滚;遇到人为误操作,用 recovery_target_time 精确回退。

6. 恢复窗口、RPO 与 RTO 怎么算

6.1 三个时间概念

备份恢复不能只看"有没有备份",要看三个时间量:

  • RPO(Recovery Point Objective,恢复点目标):最多能容忍丢多少数据,通常表现为时间。
  • RTO(Recovery Time Objective,恢复时间目标):最多能容忍业务中断多久。
  • 恢复窗口:从决定恢复到实例可用,这段过程实际花的时间,由"找备份 + 解压 + 重放 WAL + 校验 + 切流"组成。

它们的关系可以用下面的式子近似表达:

text 复制代码
可恢复的最早时间点 = 基础备份开始时刻
可恢复的最晚时间点 = 最后一个成功归档的 WAL 时刻
实际 RPO          = 当前时间 - 可恢复的最晚时间点
RTO              ≈ 备份加载时间 + WAL 重放时间 + 校验与切流时间

这意味着:即使你做了一次全量,如果没有持续归档,可恢复的最晚时间点 会一直停在备份结束点,RPO 会随时间无限变大。archive_timeout 越小,RPO 上限越小,但归档频率越高、存储和网络压力越大。

6.2 数字例子

假设数据库 200GB,pg_basebackup 速率 100MB/s,那么仅拷贝基础备份就要约 34 分钟。若每小时产生 2GB WAL,恢复到 3 小时前需要重放约 6GB WAL;在恢复机器上重放速率约 50MB/s,则需要约 2 分钟。再叠加 5 分钟校验与切流,RTO 大约 40 分钟。如果你的业务要求 RTO 小于 15 分钟,这个方案就不达标,需要考虑流复制备库做故障切换,PITR 只作为"回到过去某点"的手段。

这个例子的意义是:RTO 不是备份工具单点决定的,而是整条链路的最慢环节决定的。常见的瓶颈是基础备份太大、WAL 重放太慢、恢复机器磁盘 IO 弱、校验和切流没有自动化。

7. 恢复之后:备份校验与演练

7.1 三类校验

"备份成功"和"备份可用"是两件事。工程上至少要做三类校验:

  1. 文件级校验 :备份文件是否完整、能否解压、校验和是否匹配。pg_basebackup 的 backup_label 和 pg_verifybackup 可以帮你检查基础备份的完整性。
  2. 可启动校验 :把备份恢复成一台临时实例,确认能启动、能连接、SELECT 正常。
  3. 业务级校验:对关键表做行数、聚合值或抽样比对,确认恢复出来的数据业务上可用。

下面是一个可以周期性运行的校验脚本示例,目标是验证基础备份的完整性:

bash 复制代码
#!/usr/bin/env bash
# /usr/local/bin/verify_backup.sh
# 输入:备份目录,如 /backup/base
set -euo pipefail

BACKUP_DIR="$1"

# 1. 目录是否存在且包含 backup_label
test -f "${BACKUP_DIR}/backup_label"
echo "backup_label found:"
cat "${BACKUP_DIR}/backup_label"

# 2. 使用 pg_verifybackup 校验备份完整性
pg_verifybackup "${BACKUP_DIR}"

# 3. 检查是否存在 PostgreSQL 版本文件
test -f "${BACKUP_DIR}/PG_VERSION"
echo "PG_VERSION=$(cat ${BACKUP_DIR}/PG_VERSION)"

echo "verify ok: ${BACKUP_DIR}"

执行结果:pg_verifybackup 会输出每个文件的校验结果,如有缺失会返回非 0。这个脚本可以放进监控,每周跑一次并告警。

7.2 业务级校验的 SQL 示例

目标:在恢复出的临时实例上,确认订单表的关键指标和生产某个历史时点一致。前置环境:已启动恢复实例,端口 5433。

sql 复制代码
-- 在恢复实例上执行:检查订单总数与状态分布
SELECT status, count(*) AS cnt
FROM orders
GROUP BY status
ORDER BY status;

-- 检查最近 1 小时的订单是否存在(用于判断恢复点是否到位)
SELECT count(*) AS recent_orders
FROM orders
WHERE created_at >= now() - interval '1 hour';

-- 检查关键表的行数
SELECT relname, n_live_tup
FROM pg_stat_user_tables
WHERE relname IN ('orders', 'order_items', 'payments');

结果解读:如果 recent_orders 为 0,说明恢复点过于靠前;如果状态分布异常(例如全部是 CANCELED),说明恢复点停在了误操作之后。这些检查应写进恢复演练的 checklist,而不是靠人工临时判断。

8. 和 Spring Boot 应用怎么衔接

8.1 应用侧的配置要点

对 Spring Boot 应用来说,备份恢复不是应用直接调用的功能,但应用侧的配置会直接影响恢复质量。两个关键点:连接串要能指向恢复实例 ,写入要能快速切走。

下面是一个 application.yml 示例,目标是把数据库地址、连接池和故障切换解耦,便于恢复时改指向临时实例:

yaml 复制代码
spring:
  datasource:
    url: jdbc:postgresql://db-primary.internal:5432/orderdb?targetServerType=primary
    username: order_app
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      connection-timeout: 3000
      validation-timeout: 1000
      keepalive-time: 30000

management:
  health:
    db:
      enabled: true

targetServerType=primary 让 JDBC 驱动在多主机场景下优先选择主库;connection-timeout 和 validation-timeout 让故障实例被快速识别而不是长时间挂起。恢复期间,运维把域名 db-primary.internal 指向恢复实例即可,应用无需改代码。

8.2 一个用于恢复前"冻结写入"的最小 Spring Boot 示例

目标:在决定恢复前,先让应用停止写入,避免恢复出来的数据和正在跑的实例继续分叉。前置环境:Spring Boot 3.x + Spring Data JPA,数据库为 PostgreSQL。

java 复制代码
package com.example.backup;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 恢复窗口期间用于控制写入的开关。
 * 生产上通常由运维通过配置中心或发布系统触发。
 */
@Component
public class WriteGate implements HealthIndicator {

    private final AtomicBoolean writesEnabled = new AtomicBoolean(true);
    private final JdbcTemplate jdbcTemplate;

    public WriteGate(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * 冻结写入前,确认当前实例可连接,并记录一个用于对账的 LSN。
     * 返回的 LSN 可以作为后续恢复目标或校验参考。
     */
    public String freezeWrites() {
        writesEnabled.set(false);
        String lsn = jdbcTemplate.queryForObject(
            "SELECT pg_current_wal_lsn()::text", String.class);
        return lsn;
    }

    public void resumeWrites() {
        writesEnabled.set(true);
    }

    public boolean isWritesEnabled() {
        return writesEnabled.get();
    }

    @Override
    public Health health() {
        if (writesEnabled.get()) {
            return Health.up().withDetail("writes", "enabled").build();
        }
        return Health.outOfService().withDetail("writes", "frozen").build();
    }
}

关键步骤说明:freezeWrites() 把写入开关置为关闭,并读取当前 WAL 位点;健康检查返回 OUT_OF_SERVICE,方便负载均衡把流量摘走。恢复完成后调用 resumeWrites() 恢复写入。

适用场景与边界:这个示例只演示了"冻结开关"的思路,真实系统需要在业务写入入口(如 Service 层或拦截器)真正检查这个开关,否则只是改了一个无人读取的变量。pg_current_wal_lsn() 返回的是当前写入位点,可以作为恢复后的对账参考,但不能直接当作 PITR 目标,因为 PITR 目标通常由归档侧的时间点决定。

9. 常见误区

9.1 把 pg_dump 当 PITR 的基础

pg_dump 是逻辑备份,得到的是 SQL 或自定义格式的数据快照,不包含物理层面的 WAL。它适合迁移、单表导出、跨版本升级,但不能配合 WAL 归档做时间点恢复。想做 PITR,起点必须是物理基础备份。

9.2 以为"备份成功就是可恢复"

备份脚本返回 0 只说明文件写出去了。文件是否完整、能否启动、数据是否一致,需要单独的校验环节。没有恢复演练的备份,和没有备份差别不大。

9.3 归档目录和主库放同一块盘

这是最危险的配置之一。主库磁盘损坏时,数据文件和归档同时丢失,PITR 直接失效。归档至少要放到不同机器或对象存储。

9.4 忽略 archive_command 的失败重试特性

PostgreSQL 会反复重试同一条 archive_command,并且不会覆盖本地 WAL。如果命令因为权限或路径问题一直失败,pg_wal 会持续增长,最终写满磁盘导致主库不可写。归档失败必须告警。

9.5 恢复时忘记摘流

恢复实例启动后,如果应用连接串还指向它、且它有写入流量,可能产生新的分叉数据。恢复期间的正确顺序是:先摘流或冻结写入,再启动恢复实例,校验通过后再切流。

10. 生产实践建议

  • 基础备份和 WAL 归档必须同时存在。只有基础备份,RPO 等于备份间隔;只有归档没有基线,无法快速重建。
  • 给归档目录做容量告警和生命周期管理。WAL 归档量随写入量增长,保留策略要覆盖最长的恢复窗口需求。
  • 定期做恢复演练。至少每季度一次,从备份恢复到一台临时实例,验证可启动、可查询、关键指标正确,并记录实际 RTO。
  • 命名恢复点用于发布回滚 。在重要发布前执行 pg_create_restore_point('release_20240601'),出问题时可以按名称精确恢复。
  • 恢复目标和时区写清楚。跨时区团队尤其要统一时区,避免恢复到错误的时间点。
  • 把恢复步骤脚本化。手工恢复容易漏步骤,把拷贝、配置、启动、校验写成脚本,降低人为失误。

11. 排障清单

现象 可能原因 排查命令或位置
恢复后数据停在很早的时点 归档缺失或 restore_command 路径错误 看 pg_stat_archiver、检查归档目录文件
恢复日志报找不到 WAL 文件 归档未覆盖备份结束点之后的段 对比 backup_label 中的起始位点与归档列表
主库写入变慢、pg_wal 膨胀 archive_command 持续失败 SELECT * FROM pg_stat_archiver; 看 failed_count
恢复实例无法启动 数据目录权限、属主或配置错误 检查 postgresql.auto.conf、数据目录权限
恢复后业务校验不通过 恢复目标选错或时区偏差 检查 recovery_target_time 与时区设置
恢复耗时远超预期 基础备份过大或恢复机器 IO 弱 分阶段计时:拷贝、解压、重放、校验

12. 面试/复盘问题

  1. 为什么 PITR 必须同时依赖基础备份和 WAL 归档?缺少任一会发生什么?
  2. pg_basebackup 的 -Xs 和 -Xf 有什么区别,分别在什么场景使用?
  3. archive_command 为什么必须幂等,失败重试会对主库产生什么影响?
  4. recovery_target_time 和 recovery_target_name 各自适合什么业务场景?
  5. 如果 RTO 要求小于 15 分钟,PITR 和流复制备库各自扮演什么角色?
  6. 如何设计一套"备份是否真的可恢复"的周期性校验机制?
  7. 恢复期间应用侧需要做哪些配合动作,为什么不能只靠 DBA 操作?

13. 总结:把知识收回一张决策表

回到开头那个凌晨的场景,一个成熟的团队应该能在 40 分钟内完成:确认恢复目标时间点、在临时机器上铺开基础备份、通过归档 WAL 重放到目标点、启动实例、跑业务校验、切流。要做到这一点,靠的不是某个神奇命令,而是整条链路都经过了验证。

把本文的关键选择收进一张决策表:

你的目标 推荐手段 关键配置或命令 主要限制
回到某个时间点 基础备份 + WAL 归档 + PITR pg_basebackup、archive_command、recovery_target_time 依赖归档连续性和恢复演练
快速切换故障实例 流复制备库 + 切换 primary_conninfo、pg_ctl promote 无法回到过去,只能接管当前
单表或跨版本迁移 逻辑备份 pg_dump / pg_restore 不适合大规模 PITR
发布前打点 命名恢复点 pg_create_restore_point 需要在发布前主动执行
验证备份可用性 文件校验 + 恢复演练 pg_verifybackup、临时实例启动 需要额外资源和流程

最后记住一句话:全量基线让你有起点,WAL 归档让你能往前走,PITR 让你能停在指定的一刻,而校验和演练让你确信这三件事真的成立。

14. 参考资料

  • PostgreSQL 官方文档:Backup and Restore(Backup and Restore 章节)
  • PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery(PITR)
  • PostgreSQL 官方文档:Write-Ahead Logging(WAL)
  • PostgreSQL 官方文档:pg_basebackup
  • PostgreSQL 官方文档:pg_verifybackup
  • PostgreSQL 官方文档:pg_stat_archiver 视图
  • PostgreSQL 官方文档:Recovery Configuration(restore_command、recovery_target_* 参数)
相关推荐
摇滚侠1 小时前
Java 中如何表示负数,有符号和无符号的区别,补码的计算规则,char byte short int long 的范围
java·开发语言
奇牙coding1 小时前
Codex CLI 升级后多文件编辑静默失效:config.yaml 枚举变更排查与修复指南
java·前端·javascript·ai
小羊没烦恼!2 小时前
Ajax与JSON的一些总结
android·java·前端·数据库·mysql·算法·okhttp
注塑机数据采集联网2 小时前
如何解决注塑机数据采集联网对接MES系统数据WiFi传输数据不稳问题
大数据·数据库·物联网·数据挖掘·数据分析
Lhappy嘻嘻2 小时前
读懂 JVM:内存模型、类加载机制、分代回收
java·开发语言·jvm
黄俊懿2 小时前
【架构师从入门到进阶】第六章:服务集群优化——第八节:线程数公式
java·服务器·数据库·分布式·性能优化·架构·系统架构
Rain的Java大神之路2 小时前
别再死磕 MySQL 了:向量数据库与 RAG 生产落地实录(Java 工程师专场)
java·后端·面试
七夜zippoe2 小时前
Spring AI 是什么?Java 工程师的 AI 开发第一站
java·人工智能·spring·ai·开发
geovindu2 小时前
sql: JSON and XML Data Handling in SQL using mysql 9.0
数据库·sql·mysql·数据库开发