Skip to content

PITR 原理 ​

PITR 是 Point-In-Time Recovery(时间点恢复)的缩写,指的是在数据库发生误操作、数据损坏或逻辑错误之后,把数据库恢复到历史某一个精确时刻的状态。它依赖的机制并不是「保留多份快照」,而是「一份基础备份 + 从备份时刻开始的全部 WAL 日志」这样一个组合。只要 WAL 日志是连续的、没有断档,理论上就可以把数据库停在这段时间轴上的任意一个位置。

PostgreSQL 实现 PITR 的核心思想,是把它已经用于崩溃恢复(crash recovery)的重放机制复用起来,只是把「重放多少」这件事从「重放到 WAL 末尾」变成「重放到用户指定的目标点」。这也是为什么 PITR 不需要任何额外的专用软件:WAL 本身就是数据库正常运行必须写的日志,我们只是额外做了一件事——在 WAL 段文件被回收之前,把它们归档到一个安全的地方长期保存。

理解 PITR 有一个关键前提:逻辑备份无法做 PITR。pg_dump / pg_dumpall 产出的是「某一时刻的逻辑快照」,它只包含数据,不含 WAL,因此无法把数据库恢复到导出之后、下一次导出之前的任意时刻。能支撑连续时间点恢复的,只有物理备份(基础备份)加上连续的 WAL 归档。本页先讲清整体原理与流程,后续几页再展开归档配置、恢复配置与恢复目标类型。

前置知识

建议先阅读:物理备份与文件系统级备份

基础概念 ​

PITR 涉及的概念比较多,而且这些概念在 PostgreSQL 的其他章节(WAL、复制、运维)中反复出现。先把它们的关系理清,后面的配置与操作才不会变成「照着敲命令」。

概念英文 / 记法说明
基础备份base backup对数据目录做的物理拷贝,是恢复的起点
连续归档continuous archiving把写满或被切换的 WAL 段复制到归档目录的持续过程
WAL 段文件WAL segmentpg_wal 下 16MB 大小的日志文件,命名含时间线、日志号、段号
LSNLog Sequence NumberWAL 中的字节偏移地址,格式 X/Y
时间线timeline恢复后分叉产生的分支标识,用于区分不同恢复历史
恢复目标recovery target恢复要停在哪个时间点 / LSN / 事务 / 还原点
恢复配置recovery configuration通过 recovery.signal + 配置文件参数驱动恢复行为
RPORecovery Point Objective能容忍丢失多少数据,决定归档频率
RTORecovery Time Objective能容忍多久恢复完成,决定备份与带宽规模

基础备份 ​

基础备份是对整个数据库集簇数据目录的一致性拷贝。它必须包含数据目录、表空间目录(如果使用独立的表空间)以及 WAL 目录中的必要内容。PostgreSQL 提供了 pg_basebackup 这个客户端工具来完成这件事,它在内部通过复制协议向服务端请求一次基础备份,并保证拷贝期间数据库处于一致状态。

基础备份本身不是一个可直接启动的数据库目录(虽然在大体上它很像),它必须配合 WAL 才能恢复到一个可用状态。基础备份的价值在于:它提供了一个「已知一致」的起点,从这个起点开始重放 WAL,就能够重建后续所有状态。

连续归档 ​

连续归档是 PITR 的第二个必要组成部分。当 WAL 段写满(或被执行 pg_switch_wal() 主动切换)之后,PostgreSQL 会调用 archive_command(或 archive_library)把这个段文件复制到归档位置。归档成功后,该 WAL 段就可以在满足回收条件时从 pg_wal 中删除并复用。

pg_wal 目录的大小是有限制的,如果不做归档,写满的 WAL 段在回收后会被循环复用,历史日志就会丢失,PITR 也就无从谈起。连续归档的作用就是「在回收发生之前把日志留一份副本」。

LSN ​

LSN(Log Sequence Number)是 WAL 中的位置标记,格式为两个十六进制数,中间用斜杠分隔,例如 0/16B3748。斜杠左侧是高 32 位,右侧是低 32 位,两者拼接起来就是一个 64 位的字节偏移量,表示「从 WAL 起点算起第多少个字节」。

LSN 单调递增,是整个 PITR 体系里最精确的坐标。恢复目标可以精确到某个 LSN,恢复进度也是通过比较「已重放 LSN」与「目标 LSN」来判断的。常用的 LSN 相关函数:

函数返回用途
pg_current_wal_lsn()pg_lsn当前主库已写入 WAL 的位置
pg_last_wal_receive_lsn()pg_lsn备库已接收到的 WAL 位置
pg_last_wal_replay_lsn()pg_lsn备库已重放的 WAL 位置
pg_walfile_name(lsn)textLSN 所属的 WAL 段文件名
pg_walfile_name_offset(lsn)recordWAL 段文件名 + 段内偏移
pg_wal_lsn_diff(lsn1, lsn2)numeric两个 LSN 之间的字节差

时间线 ​

时间线(timeline)是 PITR 一个非常特殊、也非常容易被忽略的概念。每次恢复结束后,如果选择「提升(promote)」为可写的主库,PostgreSQL 会创建一个新的时间线,用一个递增的十六进制 ID 表示(初始时间线为 1)。

时间线存在的意义是:恢复可能从同一个起点分叉出多条历史。为了避免恢复时选错分支、重放「本不该发生」的 WAL,WAL 段文件名里带上了时间线 ID。恢复时通过 recovery_target_timeline 指定要跟随哪条时间线,默认值 latest 表示跟随归档目录中能找到的最新时间线。

恢复目标 ​

恢复目标(recovery target)决定恢复在哪里停下。PostgreSQL 提供了一组 recovery_target_* 参数,支持按时间、按 LSN、按事务 ID、按命名还原点四种方式,以及一个特殊的 immediate 值。

参数含义
recovery_target特殊值 immediate,表示到达一致性点立即停止
recovery_target_name按 pg_create_restore_point() 创建的命名还原点停止
recovery_target_time按时间戳停止
recovery_target_xid按事务 ID 停止
recovery_target_lsn按 WAL 位置停止
recovery_target_inclusive是否包含恰好命中的那条记录
recovery_target_timeline跟随哪条时间线
recovery_target_action到达目标后做什么:pause / promote / shutdown

这些参数的具体用法与差异,会在后续《恢复配置》《按时间 / LSN / 事务 ID 恢复》两页详细展开。

恢复信号文件 ​

在 PostgreSQL 12 之前,恢复参数是写在一个独立的 recovery.conf 文件里的,该文件一旦被处理就会被重命名为 recovery.done。从 PostgreSQL 12 开始,recovery.conf 被彻底移除,恢复参数改为写在常规配置文件(postgresql.conf / postgresql.auto.conf)中,而「这是一次恢复」这件事通过信号文件来声明。

信号文件用途创建位置
recovery.signal声明这是一次 PITR 恢复,到达目标后按 recovery_target_action 处理数据目录
standby.signal声明这是一个备库,将持续重放(不主动停止)数据目录

两个信号文件不能同时存在。如果没有信号文件,实例以正常主库方式启动;如果存在 recovery.signal,实例进入恢复模式并在满足条件后停止/提升;如果存在 standby.signal,实例进入持续恢复模式(备库),永远等待更多 WAL。

这也是为什么很多「从基础备份恢复到备库」的教程里都是 touch standby.signal:备库本质上就是「一份基础备份 + 持续重放」,与 PITR 的机制完全同源,只是一个会停、一个不会停。

RPO 与 RTO 的定义 ​

RPO 与 RTO 是灾难恢复领域最基础的两个指标,它们定义的是「业务能接受的最坏情况」。

  • RPO(Recovery Point Objective,恢复点目标):允许丢失多少时间跨度的数据。RPO = 5 分钟意味着灾难发生时最多丢 5 分钟的数据。它由「WAL 多久到达安全位置」决定。
  • RTO(Recovery Time Objective,恢复时间目标):允许服务中断多久。RTO = 1 小时意味着必须在 1 小时内恢复服务。它由「基础备份大小 + 需要重放的 WAL 量 + 恢复流程自动化程度」决定。

这两个指标是反向的:要把 RPO 压小,需要更频繁地归档 WAL(甚至用同步流复制);要把 RTO 压小,需要更快的存储、更小的基础备份、更自动化的恢复流程。二者都会推高成本,因此需要根据业务重要性分级设定。

语法与用法 ​

PITR 的「语法」并不是某一条 SQL,而是一组配置参数与操作命令的组合。整体上,一次完整的 PITR 能力建设可以分为两条主线:平时需要开启的归档链路 与 恢复时需要付出的操作。

归档链路的最小配置(postgresql.conf):

conf
# 1. 让 WAL 记录足够的信息(replica 是归档与复制的最低要求)
wal_level = replica

# 2. 打开归档开关(需要重启)
archive_mode = on

# 3. 指定归档命令(可用 SIGHUP 生效)
archive_command = 'test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f'

恢复时需要的配置文件与信号文件(位于数据目录):

bash
# 在数据目录下创建恢复信号文件,声明「这是一次 PITR 恢复」
touch /var/lib/postgresql/18/main/recovery.signal
conf
# postgresql.conf 或 postgresql.auto.conf 中追加恢复参数
restore_command = 'cp /mnt/wal_archive/%f %p'
recovery_target_time = '2026-09-20 10:30:00+08'
recovery_target_action = 'promote'

查看当前 WAL 与归档状态:

sql
-- 当前写入位置
SELECT pg_current_wal_lsn();

-- 当前 WAL 段文件名
SELECT pg_walfile_name(pg_current_wal_lsn());

-- 归档统计
SELECT archived_count, failed_count, last_archived_wal, last_failed_wal
FROM pg_stat_archiver;
text
 pg_current_wal_lsn
--------------------
 0/16B3748
(1 row)

     pg_walfile_name
-----------------------
 000000010000000000000001
(1 row)

详细说明 ​

PITR 与逻辑备份恢复的本质区别 ​

初学者最容易混淆的一点,是把「用 pg_dump 备份然后 psql 恢复」当成 PITR。这两者在能力上完全不同。

维度逻辑备份(pg_dump)物理备份 + WAL 归档(PITR)
备份内容重建数据的 SQL / 自定义格式数据数据文件的物理字节
能否恢复到任意时刻不能,只能恢复到导出的那一刻能,只要 WAL 连续
恢复粒度可单表、单 schema 恢复整个集簇级
跨大版本恢复可以(逻辑格式兼容)不可以(物理格式绑定主版本)
备份速度慢(要读出并序列化所有行)快(顺序拷贝文件)
恢复速度慢(要重放 SQL、重建索引)快(文件拷贝 + WAL 重放)
备份体积通常较小(去除了空洞)较大(含索引、空闲空间)
适用场景逻辑迁移、单对象恢复灾难恢复、误操作回退

结论:只有物理备份 + WAL 归档才能做 PITR。逻辑备份在灾难恢复中仍然有价值(例如做跨版本迁移、做小颗粒度数据找回),但它不是 PITR 的实现方式。

一次 PITR 由哪两部分组成 ​

PITR = 基础备份 + 从该基础备份起点开始的 WAL 序列。缺一不可。

  • 没有基础备份,WAL 就是一堆无法独立理解的增量记录。WAL 记录里很多内容依赖「重放前的页面状态」,无法从空目录开始重放到一致状态。
  • 没有后续 WAL,基础备份只能恢复到备份结束那一刻,之后的所有变更都无法重放。

因此,运维上必须同时保证:基础备份定期做、WAL 归档持续做且不断档。一旦归档出现断档(某个 WAL 段既不在归档里、也不在 pg_wal 里),从该断点之后再往后的所有 WAL 都无法使用,因为重放是严格有序、不允许跳跃的。

WAL 段文件命名结构 ​

理解 WAL 归档,必须先理解 WAL 段文件的命名。一个典型的段文件名是:

text
000000010000000000000001

它由三段各 8 位十六进制数字组成:

段示例值含义
时间线00000001时间线 ID,初始为 1
日志文件号00000000逻辑日志文件编号(每个日志文件由多个段组成)
段号00000001段在该日志文件内的序号

每个 WAL 段默认 16MB。段大小由 wal_segment_size 决定,而它只能在 initdb 时指定,数据库创建之后无法修改。这意味着 PITR 恢复时目标集簇的段大小必须与备份来源一致,否则段文件名对不上,归档文件无法被正确识别。

日志文件号的递增,与段号满了之后进位有关。假设段大小 16MB、wal_segment_size 对应每日志文件 256 个段(这部分由段大小推导),当段号递增到该日志文件满后,日志文件号加一、段号归零。归档目录里看到的段文件名是连续的,但在跨日志文件边界处会有明显跳变。

为什么归档命令必须幂等 ​

archive_command(以及 archive_library)可能因为各种原因被重复调用同一个 WAL 段——例如命令实际上成功了,但由于网络抖动导致 PostgreSQL 认为它失败了,于是稍后重试。如果归档命令不是幂等的,重试就可能覆盖或破坏已经归档好的文件。

官方给出的范式是「先判断目标文件是否已存在,不存在才复制」:

bash
test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f

这条命令的语义是:只有当归档目录中还不存在同名文件时才执行复制,从而避免覆盖已有归档。需要注意的是,当目标文件已存在时,test ! -f 返回非 0,整个命令也返回非 0,PostgreSQL 会认为归档失败并稍后重试。官方对此的期望是:若已存在文件与待归档 WAL 内容一致且已完全落盘,应返回 0;内容不一致时则必须返回非 0。因此实际生产脚本通常写成「已存在则 exit 0,否则复制到临时文件再原子改名」,语义更严谨。详细讨论见《WAL 归档配置》一页。

如果直接把 cp 作为归档命令:

bash
cp %p /mnt/wal_archive/%f

在重复调用时虽然通常也能成功(cp 会覆盖),但存在两个隐患:一是在复制过程中如果被中断,会留下一个「半截」的归档文件;二是它会覆盖已有归档,一旦两个实例被误指向同一个归档目录,就可能互相破坏档案。因此不推荐。

归档失败会有什么后果 ​

归档失败(archive_command 返回非 0)会带来一系列连锁反应:

  1. 该 WAL 段无法被回收,pg_wal 里会继续保留它。
  2. 归档进程会周期性重试(重试间隔由 archive_timeout 相关逻辑与内部机制控制,实际是不断重试)。
  3. 如果归档长期失败,pg_wal 里的段会越积越多,最终耗尽磁盘。
  4. pg_wal 写满会导致 PostgreSQL 发生 PANIC 级别的错误并关闭,这是非常严重的生产事故。

因此归档监控是 PITR 体系的生命线。pg_stat_archiver 的 failed_count 与 last_failed_wal 必须纳入告警。

时间线与恢复分叉 ​

时间线的概念在第一次做 PITR 时最容易被忽略。设想这样的场景:

  • 主库在时间线 1 上运行,WAL 段名形如 00000001...。
  • 某天发生误操作,用基础备份恢复,恢复到误操作之前,并提升为新主库。
  • 提升时 PostgreSQL 创建时间线 2,之后新写入的 WAL 段名变成 00000002...。

此时归档目录里同时存在时间线 1 和后期的 WAL 段。如果再想恢复,就必须明确「要跟随哪条时间线」。recovery_target_timeline = 'latest' 会跟随归档中可用的最新时间线,这在绝大多数场景下是正确的;但如果要恢复到时间线分叉之前的某个点,就不应该无条件跟随 latest。

可以通过 pg_walfile_name(pg_current_wal_lsn()) 的前 8 位判断当前处于哪条时间线。恢复过程中,恢复日志里也会输出类似「switching to timeline 2」的信息。

恢复流程的八个阶段 ​

一次标准的 PITR 恢复,从准备到可用大致要经过以下阶段。这个流程在后续《恢复配置》一页会配合具体参数再展开,这里先给出全景。

  1. 确认目标:明确要恢复到哪个时间点 / LSN / 事务 / 还原点,并记录下目标值。
  2. 停止服务:停止正在运行的目标实例,避免文件被占用或状态被改写。
  3. 保全现场:把现有数据目录改名备份(例如 mv data data.old),为恢复失败留退路。
  4. 恢复基础备份:把基础备份解压/拷贝到数据目录,注意权限与属主。
  5. 放置归档 WAL:可以先把归档中需要的 WAL 段拷贝到 pg_wal,也可以依赖 restore_command 按需拉取。
  6. 写恢复配置:创建 recovery.signal,设置 restore_command 与 recovery_target_*。
  7. 启动并观察:启动服务,观察日志中的重放进度,确认停在预期位置。
  8. 提升并验证:确认 recovery_target_action 行为(promote 自动提升),验证数据、切换应用、恢复常规备份。

归档与恢复的时序关系 ​

archive_mode 与 archive_command 是在做什么,可以用一句话概括:在 WAL 段被回收之前,把它复制一份出去。这里的「之前」很关键——归档是唯一能阻止 WAL 段被删除的理由。

状态该段的归属能否用于恢复
正在写入位于 pg_wal,尚未写满不能(未完整)
已写满、已归档归档目录有副本,pg_wal 中可回收能
已写满、未归档只能在 pg_wal 中找到能(若尚在)
已回收、已归档只在归档目录能
已回收、未归档不存在不能(永久丢失)

这张表说明了归档的紧迫性:只要某个段进入「已回收、未归档」状态,这段历史就永久无法重放。生产环境中,pg_wal 的回收速度与 archive_command 的成功速度之间必须留有余量。

流复制与 PITR 的关系 ​

流复制与 PITR 在底层机制上是同一套东西:都是「基础备份 + WAL 重放」。区别只在于 WAL 从哪里来、以及重放什么时候停。

维度流复制(streaming replication)PITR(归档恢复)
WAL 来源直接从主库 TCP 流式传输从归档目录 / 归档存储读取
重放是否停止不停止,持续跟随主库到达 recovery_target 后停止
主要用途高可用、读写分离、降低 RPO灾难恢复、误操作回退
是否防逻辑损坏不防(损坏会同步)防(可回到损坏前)
网络依赖强依赖实时连通性弱(恢复时可离线)

因此,生产上通常是两者都要:流复制负责「主库挂了迅速切换」,PITR 负责「数据被写坏了回到过去」。这两条腿缺一条都会在特定故障下束手无策。

一个值得强调的细节是:PITR 恢复时既可以从归档读 WAL,也可以同时配置 primary_conninfo 从主库流式拉取 WAL(即「归档 + 流复制」混合模式)。混合模式在做「克隆一个历史时刻的实例」时很有用——历史 WAL 从归档读,最新 WAL 从主库读,可以少归档一部分。

PITR 的典型使用场景 ​

判断一个场景是否适合用 PITR,关键看「问题是否可以定位到一个时间点」。

场景是否适合 PITR说明
误执行 DELETE / UPDATE 无 WHERE适合恢复到语句执行前即可
误 DROP TABLE适合恢复到 drop 前,再单独导出该表数据
程序 bug 批量写坏数据适合(如果能定位首次写入时间)恢复到 bug 首次触发前
磁盘物理损坏适合从异地基础备份 + 归档恢复
数据库文件被误删适合同上
只想找回某张表的几行数据部分适合PITR 恢复整个实例代价大,可考虑逻辑备份或逻辑复制
主库宕机需要秒级切换不适合应该用流复制 / 高可用方案
跨大版本升级不适合应用逻辑方式或 pg_upgrade

对于「只想找回某张表」,一个常用的折中做法是:把 PITR 恢复到一个临时实例(同机不同端口或另一台机器),从临时实例里 pg_dump 出目标表,再导入生产库。这样既拿到了过去的数据,又不影响生产实例的历史。

归档与恢复的时间线示意图 ​

用文字描述一次完整的生命周期,有助于建立整体图景:

text
时间轴 ──────────────────────────────────────────────────────────►

[基础备份 B1]        [误操作 !]        [发现并决定恢复]
     │                    │                    │
     ├── WAL 段 001 ──────┤
     │                    ├── WAL 段 002 ──────┤
     │                    │                    ├── WAL 段 003 ──► (持续归档)
     │                    │
     └────────── 恢复时:拷贝 B1,设置 recovery_target
                  停在「WAL 段 002 中的误操作之前」

要点在于:

  • 基础备份只是起点,真正的「时间能力」来自后面连续的 WAL。
  • 误操作本身也会被写进 WAL(DELETE 是一条 WAL 记录),因此恢复时必须把目标设在它之前。
  • 误操作之后、发现之前的 WAL 依然要保留并归档,因为「发现误操作」往往需要时间,这段 WAL 反而是恢复到更早时刻的依据。

归档存储的物理分离 ​

一个常见但危险的做法,是把归档目录放在数据库服务器本地磁盘上。此时磁盘损坏、机房故障、误删目录这三类事故都会让「数据库」和「归档」同时失效,PITR 也就失去了意义。

这意味着「归档目录」不能和数据目录放在同一块磁盘、甚至不能放在同一个存储系统上。归档的目标位置应当是独立的存储介质,最好再通过独立通道同步到异地。

恢复时的目标值从哪来 ​

实操中最难的往往不是「怎么恢复」,而是「恢复到哪个时刻」。目标值通常来自以下途径:

  • 操作审计日志:应用层或数据库层的审计记录,包含误操作的发生时间。
  • WAL 内容分析:用 pg_waldump 读取 WAL,找到具体事务的提交位置与 LSN。
  • 监控系统记录:pg_stat_activity 采样、慢查询日志等,帮助回溯操作发生区间。
  • 命名还原点:如果事前打过还原点,直接用它,最可靠。

如果能准确知道「误操作事务的提交时间」,用 recovery_target_time 略早于该时间即可;如果只能定位到某个 LSN,用 recovery_target_lsn 更精确。而 pg_create_restore_point() 的价值就在于:它把「一个业务上可识别的节点」固化成一个有名字的目标值,避免事后去猜。

WAL 归档与 WAL 回收的先后关系 ​

pg_wal 目录不能无限增长,写满的 WAL 段在满足条件后会被回收。回收的前提之一是「该段已经成功归档(如果开启了归档)」。因此:

  • 归档正常 → 段可以被回收 → pg_wal 大小稳定;
  • 归档失败或滞后 → 段无法回收 → pg_wal 持续膨胀。

这就是为什么「归档失败」通常不会立刻表现为报错,而是先表现为磁盘空间缓慢增长。监控 pg_wal 目录大小,可以比 pg_stat_archiver 更早暴露问题。

备份校验与「能恢复」是两件事 ​

很多团队把「备份任务成功」等同于「备份可用」,这是一个危险的假设。备份任务成功只说明文件被写出来了,不代表:

  • 文件完整(可能被截断、被并发覆盖);
  • 文件可解压、可拷贝回数据目录;
  • 目标集簇能基于它启动并重放到预期状态;
  • 依赖的 WAL 归档确实连续、没有缺口。

因此备份体系里必须有「校验 + 演练」这两个环节:pg_verifybackup 之类的工具负责校验备份清单与文件校验和,恢复演练负责端到端验证。二者结合才能把「备份成功」升级为「恢复可行」。

常见误解澄清 ​

PITR 有几个反复出现的误解,逐个澄清有助于建立正确的心智模型。

误解事实
「有了流复制就不需要 PITR」流复制会同步传播逻辑损坏(误删、误更新),救不了这类故障
「pg_dump 备份能恢复到任意时刻」逻辑备份不含 WAL,只能恢复到导出时刻
「基础备份就是可启动的数据库目录」基础备份是恢复起点,必须配合 WAL 重放才是一致状态
「归档失败等一会儿自己会好」归档失败会导致 pg_wal 膨胀,最终 PANIC 关闭
「恢复到误操作前即可,inclusive 不重要」边界是否包含会导致差一条记录,recovery_target_inclusive 需要明确
「恢复完就万事大吉」恢复并提升后是新时间线,必须重建备份与归档链路
「WAL 归档目录放在数据盘旁边就行」磁盘/机房故障会同时带走两者,归档必须异地

恢复时间的经验估算 ​

RTO 主要由三部分构成,可以粗略分解估算:

text
RTO ≈ 备份拷贝/解压时间 + WAL 重放时间 + 验证与切换时间

以一份 500GB 的基础备份为例,若存储读取带宽 200MB/s,拷贝本身约需 40 分钟以上;再加上从备份时刻到恢复目标之间累计的 WAL 重放时间,以及应用连接切换、数据校验的时间,整体 RTO 很容易超过 1 小时。这也是为什么「全量备份 + 增量」的分层策略在实际生产中被广泛采用——把恢复时需要重放的 WAL 量控制在合理范围。

缩短 RTO 的常见手段:使用更快的存储(SSD/本地盘)、把基础备份做成已解压的目录、提前预热归档缓存、把恢复流程自动化为一条命令、并定期演练以消除操作上的犹豫时间。

基础备份的三种获取方式 ​

获取基础备份并非只有 pg_basebackup 一种途径,理解这三种方式的差异有助于在不同场景下做选择。

方式做法特点
pg_basebackup通过复制协议向服务端请求最常用,自动处理一致性,支持 tar / plain 格式
低层 APIpg_backup_start() / pg_backup_stop() + 文件系统快照适合与存储快照集成,需要自己保证一致
文件系统快照在 pg_backup_start() 之后做 LVM/ZFS 快照秒级完成,适合大库;依赖存储能力

无论哪种方式,产出的基础备份都必须包含「备份起始时的 WAL 位置」这一元信息(通常记录在 backup_label 中),恢复时才知道从哪里开始重放。低层 API 方式下,pg_backup_start() 返回起始 LSN,pg_backup_stop() 返回结束 LSN 与标签文件内容,这些都要妥善保存。

WAL 归档模式的演进 ​

PostgreSQL 的归档配置方式经历过一次重要演进。在较早期版本中,归档只能通过 archive_command(调用外部命令)实现;从 PostgreSQL 15 开始,新增了 archive_library 参数,允许用共享库来实现归档,避免每次归档都 fork 一个外部进程,在高写入场景下能显著降低开销。

参数引入版本机制与另一个参数的关系
archive_command早期版本调用 shell 命令与 archive_library 互斥
archive_libraryPostgreSQL 15加载共享库回调与 archive_command 互斥

两者只能设置一个。如果都为空,归档不会发生(即使 archive_mode = on)。如果都设置了,服务端会报错。选择哪一个取决于运维习惯:archive_command 简单直观、易于用脚本实现复杂逻辑;archive_library 性能更好,但需要编译与部署共享库。

恢复过程中可以观察什么 ​

恢复期间,服务端日志会输出重放进度。可以通过日志关键字判断当前状态:

text
LOG:  starting point-in-time recovery to 2026-09-20 10:30:00+08
LOG:  restored log file "0000000100000000000000B2" from archive
LOG:  redo starts at 0/16000028
LOG:  consistent recovery state reached at 0/16B3748
LOG:  recovery stopping before commit of transaction 743, time 2026-09-20 10:31:12.339251+08
LOG:  redo done at 0/16C4D00
LOG:  selected new timeline ID: 3
LOG:  archive recovery complete

关键行义:

  • restored log file ... 表示正在从归档拉取 WAL;
  • consistent recovery state reached 表示已到达一致点,可以对外服务(只读);
  • recovery stopping before commit of transaction 表示命中了恢复目标;
  • selected new timeline ID 表示提升后创建的新时间线。

实战示例 ​

示例一:确认当前归档链路是否健康 ​

在假设 PITR 可用之前,先验证归档确实在正常工作。

sql
-- 查看关键参数
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('wal_level', 'archive_mode', 'archive_command', 'archive_library', 'archive_timeout');
text
      name       |              setting               | unit
-----------------+------------------------------------+------
 archive_command | test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f |
 archive_library |                                    |
 archive_mode    | on                                 |
 archive_timeout | 300                                | s
 wal_level       | replica                            |
(5 rows)
sql
-- 查看归档统计:失败为 0 且最近一次归档时间较新,才算健康
SELECT
    archived_count,
    failed_count,
    last_archived_wal,
    last_archived_time,
    last_failed_wal,
    last_failed_time,
    stats_reset
FROM pg_stat_archiver;
text
 archived_count | failed_count |   last_archived_wal    |  last_archived_time  | last_failed_wal | last_failed_time |         stats_reset
----------------+--------------+------------------------+----------------------+-----------------+------------------+-------------------------------
            182 |            0 | 0000000100000000000000B6 | 2026-09-28 09:41:12+08 |                 |                  | 2026-09-01 00:00:03.112+08
(1 row)

示例二:制造一个命名还原点作为恢复目标 ​

在做高风险操作(例如批量数据修复、大版本升级前的数据重写)之前,先打一个命名还原点。这样一旦出错,就可以精确恢复到该点。

sql
-- 创建一个命名还原点
SELECT pg_create_restore_point('before_batch_fix_20260928');
text
   pg_create_restore_point
--------------------------
 0/16C4D00
(1 row)

还原点会被写进 WAL 并强制切换 WAL 段,从而保证它一定能被归档。恢复时用:

conf
recovery_target_name = 'before_batch_fix_20260928'
recovery_target_action = 'promote'

示例三:估算一段 WAL 覆盖了多长时间 ​

在评估「WAL 归档能覆盖多久」时,需要知道单位时间内产生多少 WAL。可以通过在两次采样之间做 LSN 差值来估算。

sql
-- 第一次采样
SELECT pg_current_wal_lsn() AS lsn1, now() AS t1;
text
   lsn1    |              t1
-----------+-------------------------------
 0/16B3748 | 2026-09-28 09:30:00.000000+08
(1 row)
sql
-- 30 分钟后第二次采样
SELECT pg_current_wal_lsn() AS lsn2, now() AS t2,
       pg_wal_lsn_diff(pg_current_wal_lsn(), '0/16B3748') AS bytes_since;
text
   lsn2    |              t2               | bytes_since
-----------+-------------------------------+-------------
 0/1A9B120 | 2026-09-28 10:00:00.000000+08 |    45339608
(1 row)

30 分钟产生约 43MB WAL,按此速率一天约 2GB。这个数字直接决定了归档存储容量与归档带宽需求。

示例四:检查 WAL 归档是否存在缺口 ​

归档链路是否连续,可以直接在归档目录里看文件名的连续性。用 ls 排序列出归档 WAL 段:

bash
ls -1 /mnt/wal_archive/ | grep -E '^[0-9A-F]{24}$' | sort
text
0000000100000000000000B0
0000000100000000000000B1
0000000100000000000000B2
0000000100000000000000B4
0000000100000000000000B5

上面的输出里,...B2 之后直接跳到 ...B4,缺少 ...B3,说明存在一个缺口。这个缺口会导致从 ...B3 开始之后的所有 WAL 都无法用于恢复(因为重放不允许跳跃)。此时应立即:

  1. 检查 pg_wal 目录里是否还有 ...B3(可能只是还没归档);
  2. 检查 pg_stat_archiver 里 last_failed_wal 是否为该段;
  3. 检查归档目录是否被外部进程误删文件。

示例五:查看当前时间线 ​

时间线信息对恢复目标的选择很关键。最直接的判断方式是看当前 WAL 段文件名的前 8 位:

sql
SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_walfile,
       substring(pg_walfile_name(pg_current_wal_lsn()), 1, 8) AS timeline_hex;
text
        current_walfile        | timeline_hex
-------------------------------+--------------
 0000000200000000000000C1      | 00000002
(1 row)

00000002 表示当前处于时间线 2,也就是说这个实例曾经被恢复并提升过一次。恢复时如果设置 recovery_target_timeline = 'latest',就会跟随到时间线 2;如果要恢复到分叉之前的状态,则应显式指定时间线 1。

注意事项 ​

归档目录的权限要严格控制。 WAL 归档文件包含数据库的全部变更,等同于数据库的明文内容。归档目录应该是 0700,并且属主与 PostgreSQL 运行用户一致,避免被同机其他用户读取。

wal_level 的修改需要重启。 wal_level 不是可通过 reload 生效的参数。如果当前是 minimal(很多只读或单机场景的默认值在旧版本中是 minimal),必须重启并重建基础备份,重启之前产生的 WAL 与旧的基础备份在 replica 下未必可用(minimal 模式下部分操作不写 WAL,例如 CREATE TABLE、CLUSTER、TRUNCATE 等)。

不要把 archive_command 写成会阻塞的交互式命令。 归档命令在后台进程中被同步调用,如果它试图打开终端、读取标准输入或挂起,会拖垮归档链路,进而导致 pg_wal 堆积。

归档成功的判定标准只有退出码。 PostgreSQL 只看 archive_command/archive_library 的返回值是否为 0。如果命令语法错误但恰好返回 0,PostgreSQL 会认为归档成功并回收 WAL,而实际上归档目录里什么都没有——这是一个非常隐蔽的定时炸弹。归档命令本身要经过测试。

恢复时不要忘记 restore_command 的失败语义。 restore_command 如果找不到请求的 WAL 文件,应当立即返回非 0,而不是等待或重试。服务端自己会按需重试调用。如果命令内部做了长时间等待,会拖慢恢复。

恢复目标必须落在 WAL 覆盖范围内。 如果 recovery_target_time 指定的时间晚于已归档 WAL 的末尾,恢复会一直重放到 WAL 末尾然后报错(或停在末尾),此时该时间点实际上无法到达。恢复前应先确认「目标时间 ≤ 归档中最后一条 WAL 的覆盖时间」。

恢复实例与源实例的主版本必须一致。 物理备份与 WAL 绑定 PostgreSQL 主版本(例如都是 18),跨大版本的物理恢复不受支持。跨版本迁移应使用逻辑方式(pg_dump / pg_upgrade)。

恢复期间不要有其他进程写入数据目录。 恢复过程中数据目录处于「不可并发访问」的状态,任何外部进程(包括备份软件、文件同步工具、手工 cp)都不得同时读写,否则可能破坏文件一致性。

不要在 pg_wal 里手工删除文件。 pg_wal 中的文件由 PostgreSQL 自己管理生命周期,手工删除会直接造成 WAL 断档。要减少 pg_wal 占用,应通过修复归档、调整 max_slot_wal_keep_size 等正常手段。

恢复后的实例必须重建备份策略。 恢复并提升之后,时间线发生变化,新实例需要立即恢复 WAL 归档链路并重新做一次基础备份,否则这个新实例实际上处于「无保护」状态。

最佳实践 ​

  1. 把「基础备份 + WAL 归档」作为一个整体来设计。 不要只做其中一半。只做基础备份,RPO 会等于备份间隔(可能长达一天);只做归档而不做基础备份,则没有恢复起点。

  2. 归档命令必须幂等。 使用 test ! -f ... && cp ... 这样的范式,保证重复调用不破坏已有归档。归档命令上线前务必在测试环境演练一遍。

  3. 对归档失败设置一级告警。 监控 pg_stat_archiver.failed_count 的增长以及 last_failed_wal 的变化。归档失败往往是磁盘满、权限丢失、远端挂载点掉线的第一信号。注意:因信号终止或 shell 退出码大于 125 导致的失败不会记入该视图,还需结合日志一起看。

  4. 监控 pg_wal 目录大小。 设置阈值告警(例如超过基础水位 2 倍)。pg_wal 持续增长通常意味着归档链路出了问题,而不是「日志变多了」。

  5. 开启 archive_timeout 兜底。 即使没有写满 WAL 段,也强制每隔一段时间切换一次,保证归档的时延有上界。低写入量的库尤其需要。

  6. 定期做恢复演练。 备份的价值只有在恢复成功时才成立。至少每季度做一次真实的 PITR 演练,记录 RTO 并验证 RPO。

  7. 每个高风险操作前创建命名还原点。 用 pg_create_restore_point('描述性名称') 标记关键节点,为精确恢复提供锚点。

  8. 恢复时先保全现场。 在动数据目录之前,把它改名而不是删除。恢复失败时可以快速回滚,避免「恢复不成反而把好数据弄丢」。

  9. 记录每次恢复的时间线变化。 恢复并提升后时间线会递增,运维文档里应记录时间线切换的时间与原因,避免后续恢复时选错时间线。

  10. 对归档存储做异地冗余。 归档存储如果与数据库在同一台机器或同一个机房,一次机房级故障就会同时带走数据库和归档。归档必须异地留一份(详见灾难恢复章节)。

  11. 定期校验归档文件的完整性。 归档文件应当尽量避免被篡改或截断。可以用文件大小与校验和做周期性抽查,及早发现损坏。

  12. 把恢复步骤写成可执行的脚本或 Runbook。 灾难发生时人容易紧张、容易漏步骤。把第 3 节的八阶段流程固化成脚本与检查清单,可以显著降低 RTO 与人为失误。

  13. 明确区分「归档」与「备份」两个概念。 归档保存的是增量 WAL,备份保存的是一致性快照。二者职责不同,监控指标、保留周期、存储位置都应当分开设计,不要混为一谈。

  14. 给恢复流程设定一个「决策截止点」。 并不是所有故障都值得做 PITR(全实例恢复代价高)。在 Runbook 里写清「什么故障做 PITR、什么故障走单表逻辑恢复」,可以避免在事故中临时判断。

参考链接 ​