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 segment | pg_wal 下 16MB 大小的日志文件,命名含时间线、日志号、段号 |
| LSN | Log Sequence Number | WAL 中的字节偏移地址,格式 X/Y |
| 时间线 | timeline | 恢复后分叉产生的分支标识,用于区分不同恢复历史 |
| 恢复目标 | recovery target | 恢复要停在哪个时间点 / LSN / 事务 / 还原点 |
| 恢复配置 | recovery configuration | 通过 recovery.signal + 配置文件参数驱动恢复行为 |
| RPO | Recovery Point Objective | 能容忍丢失多少数据,决定归档频率 |
| RTO | Recovery 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) | text | LSN 所属的 WAL 段文件名 |
pg_walfile_name_offset(lsn) | record | WAL 段文件名 + 段内偏移 |
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):
# 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'恢复时需要的配置文件与信号文件(位于数据目录):
# 在数据目录下创建恢复信号文件,声明「这是一次 PITR 恢复」
touch /var/lib/postgresql/18/main/recovery.signal# 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 与归档状态:
-- 当前写入位置
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; 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 段文件的命名。一个典型的段文件名是:
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 认为它失败了,于是稍后重试。如果归档命令不是幂等的,重试就可能覆盖或破坏已经归档好的文件。
官方给出的范式是「先判断目标文件是否已存在,不存在才复制」:
test ! -f /mnt/wal_archive/%f && cp %p /mnt/wal_archive/%f这条命令的语义是:只有当归档目录中还不存在同名文件时才执行复制,从而避免覆盖已有归档。需要注意的是,当目标文件已存在时,test ! -f 返回非 0,整个命令也返回非 0,PostgreSQL 会认为归档失败并稍后重试。官方对此的期望是:若已存在文件与待归档 WAL 内容一致且已完全落盘,应返回 0;内容不一致时则必须返回非 0。因此实际生产脚本通常写成「已存在则 exit 0,否则复制到临时文件再原子改名」,语义更严谨。详细讨论见《WAL 归档配置》一页。
如果直接把 cp 作为归档命令:
cp %p /mnt/wal_archive/%f在重复调用时虽然通常也能成功(cp 会覆盖),但存在两个隐患:一是在复制过程中如果被中断,会留下一个「半截」的归档文件;二是它会覆盖已有归档,一旦两个实例被误指向同一个归档目录,就可能互相破坏档案。因此不推荐。
归档失败会有什么后果
归档失败(archive_command 返回非 0)会带来一系列连锁反应:
- 该 WAL 段无法被回收,
pg_wal里会继续保留它。 - 归档进程会周期性重试(重试间隔由
archive_timeout相关逻辑与内部机制控制,实际是不断重试)。 - 如果归档长期失败,
pg_wal里的段会越积越多,最终耗尽磁盘。 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 恢复,从准备到可用大致要经过以下阶段。这个流程在后续《恢复配置》一页会配合具体参数再展开,这里先给出全景。
- 确认目标:明确要恢复到哪个时间点 / LSN / 事务 / 还原点,并记录下目标值。
- 停止服务:停止正在运行的目标实例,避免文件被占用或状态被改写。
- 保全现场:把现有数据目录改名备份(例如
mv data data.old),为恢复失败留退路。 - 恢复基础备份:把基础备份解压/拷贝到数据目录,注意权限与属主。
- 放置归档 WAL:可以先把归档中需要的 WAL 段拷贝到
pg_wal,也可以依赖restore_command按需拉取。 - 写恢复配置:创建
recovery.signal,设置restore_command与recovery_target_*。 - 启动并观察:启动服务,观察日志中的重放进度,确认停在预期位置。
- 提升并验证:确认
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 出目标表,再导入生产库。这样既拿到了过去的数据,又不影响生产实例的历史。
归档与恢复的时间线示意图
用文字描述一次完整的生命周期,有助于建立整体图景:
时间轴 ──────────────────────────────────────────────────────────►
[基础备份 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 主要由三部分构成,可以粗略分解估算:
RTO ≈ 备份拷贝/解压时间 + WAL 重放时间 + 验证与切换时间以一份 500GB 的基础备份为例,若存储读取带宽 200MB/s,拷贝本身约需 40 分钟以上;再加上从备份时刻到恢复目标之间累计的 WAL 重放时间,以及应用连接切换、数据校验的时间,整体 RTO 很容易超过 1 小时。这也是为什么「全量备份 + 增量」的分层策略在实际生产中被广泛采用——把恢复时需要重放的 WAL 量控制在合理范围。
缩短 RTO 的常见手段:使用更快的存储(SSD/本地盘)、把基础备份做成已解压的目录、提前预热归档缓存、把恢复流程自动化为一条命令、并定期演练以消除操作上的犹豫时间。
基础备份的三种获取方式
获取基础备份并非只有 pg_basebackup 一种途径,理解这三种方式的差异有助于在不同场景下做选择。
| 方式 | 做法 | 特点 |
|---|---|---|
pg_basebackup | 通过复制协议向服务端请求 | 最常用,自动处理一致性,支持 tar / plain 格式 |
| 低层 API | pg_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_library | PostgreSQL 15 | 加载共享库回调 | 与 archive_command 互斥 |
两者只能设置一个。如果都为空,归档不会发生(即使 archive_mode = on)。如果都设置了,服务端会报错。选择哪一个取决于运维习惯:archive_command 简单直观、易于用脚本实现复杂逻辑;archive_library 性能更好,但需要编译与部署共享库。
恢复过程中可以观察什么
恢复期间,服务端日志会输出重放进度。可以通过日志关键字判断当前状态:
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 可用之前,先验证归档确实在正常工作。
-- 查看关键参数
SELECT name, setting, unit
FROM pg_settings
WHERE name IN ('wal_level', 'archive_mode', 'archive_command', 'archive_library', 'archive_timeout'); 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)-- 查看归档统计:失败为 0 且最近一次归档时间较新,才算健康
SELECT
archived_count,
failed_count,
last_archived_wal,
last_archived_time,
last_failed_wal,
last_failed_time,
stats_reset
FROM pg_stat_archiver; 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)示例二:制造一个命名还原点作为恢复目标
在做高风险操作(例如批量数据修复、大版本升级前的数据重写)之前,先打一个命名还原点。这样一旦出错,就可以精确恢复到该点。
-- 创建一个命名还原点
SELECT pg_create_restore_point('before_batch_fix_20260928'); pg_create_restore_point
--------------------------
0/16C4D00
(1 row)还原点会被写进 WAL 并强制切换 WAL 段,从而保证它一定能被归档。恢复时用:
recovery_target_name = 'before_batch_fix_20260928'
recovery_target_action = 'promote'示例三:估算一段 WAL 覆盖了多长时间
在评估「WAL 归档能覆盖多久」时,需要知道单位时间内产生多少 WAL。可以通过在两次采样之间做 LSN 差值来估算。
-- 第一次采样
SELECT pg_current_wal_lsn() AS lsn1, now() AS t1; lsn1 | t1
-----------+-------------------------------
0/16B3748 | 2026-09-28 09:30:00.000000+08
(1 row)-- 30 分钟后第二次采样
SELECT pg_current_wal_lsn() AS lsn2, now() AS t2,
pg_wal_lsn_diff(pg_current_wal_lsn(), '0/16B3748') AS bytes_since; lsn2 | t2 | bytes_since
-----------+-------------------------------+-------------
0/1A9B120 | 2026-09-28 10:00:00.000000+08 | 45339608
(1 row)30 分钟产生约 43MB WAL,按此速率一天约 2GB。这个数字直接决定了归档存储容量与归档带宽需求。
示例四:检查 WAL 归档是否存在缺口
归档链路是否连续,可以直接在归档目录里看文件名的连续性。用 ls 排序列出归档 WAL 段:
ls -1 /mnt/wal_archive/ | grep -E '^[0-9A-F]{24}$' | sort0000000100000000000000B0
0000000100000000000000B1
0000000100000000000000B2
0000000100000000000000B4
0000000100000000000000B5上面的输出里,...B2 之后直接跳到 ...B4,缺少 ...B3,说明存在一个缺口。这个缺口会导致从 ...B3 开始之后的所有 WAL 都无法用于恢复(因为重放不允许跳跃)。此时应立即:
- 检查
pg_wal目录里是否还有...B3(可能只是还没归档); - 检查
pg_stat_archiver里last_failed_wal是否为该段; - 检查归档目录是否被外部进程误删文件。
示例五:查看当前时间线
时间线信息对恢复目标的选择很关键。最直接的判断方式是看当前 WAL 段文件名的前 8 位:
SELECT pg_walfile_name(pg_current_wal_lsn()) AS current_walfile,
substring(pg_walfile_name(pg_current_wal_lsn()), 1, 8) AS timeline_hex; 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 归档链路并重新做一次基础备份,否则这个新实例实际上处于「无保护」状态。
最佳实践
把「基础备份 + WAL 归档」作为一个整体来设计。 不要只做其中一半。只做基础备份,RPO 会等于备份间隔(可能长达一天);只做归档而不做基础备份,则没有恢复起点。
归档命令必须幂等。 使用
test ! -f ... && cp ...这样的范式,保证重复调用不破坏已有归档。归档命令上线前务必在测试环境演练一遍。对归档失败设置一级告警。 监控
pg_stat_archiver.failed_count的增长以及last_failed_wal的变化。归档失败往往是磁盘满、权限丢失、远端挂载点掉线的第一信号。注意:因信号终止或 shell 退出码大于 125 导致的失败不会记入该视图,还需结合日志一起看。监控
pg_wal目录大小。 设置阈值告警(例如超过基础水位 2 倍)。pg_wal持续增长通常意味着归档链路出了问题,而不是「日志变多了」。开启
archive_timeout兜底。 即使没有写满 WAL 段,也强制每隔一段时间切换一次,保证归档的时延有上界。低写入量的库尤其需要。定期做恢复演练。 备份的价值只有在恢复成功时才成立。至少每季度做一次真实的 PITR 演练,记录 RTO 并验证 RPO。
每个高风险操作前创建命名还原点。 用
pg_create_restore_point('描述性名称')标记关键节点,为精确恢复提供锚点。恢复时先保全现场。 在动数据目录之前,把它改名而不是删除。恢复失败时可以快速回滚,避免「恢复不成反而把好数据弄丢」。
记录每次恢复的时间线变化。 恢复并提升后时间线会递增,运维文档里应记录时间线切换的时间与原因,避免后续恢复时选错时间线。
对归档存储做异地冗余。 归档存储如果与数据库在同一台机器或同一个机房,一次机房级故障就会同时带走数据库和归档。归档必须异地留一份(详见灾难恢复章节)。
定期校验归档文件的完整性。 归档文件应当尽量避免被篡改或截断。可以用文件大小与校验和做周期性抽查,及早发现损坏。
把恢复步骤写成可执行的脚本或 Runbook。 灾难发生时人容易紧张、容易漏步骤。把第 3 节的八阶段流程固化成脚本与检查清单,可以显著降低 RTO 与人为失误。
明确区分「归档」与「备份」两个概念。 归档保存的是增量 WAL,备份保存的是一致性快照。二者职责不同,监控指标、保留周期、存储位置都应当分开设计,不要混为一谈。
给恢复流程设定一个「决策截止点」。 并不是所有故障都值得做 PITR(全实例恢复代价高)。在 Runbook 里写清「什么故障做 PITR、什么故障走单表逻辑恢复」,可以避免在事故中临时判断。
参考链接
- PostgreSQL 官方文档:Continuous Archiving and Point-in-Time Recovery (PITR)
- PostgreSQL 官方文档:Write-Ahead Logging (WAL)
- PostgreSQL 官方文档:Write Ahead Log — Configuration Parameters
- PostgreSQL 官方文档:Backup and Restore
- PostgreSQL 官方文档:pg_basebackup
- PostgreSQL 官方文档:Recovery Configuration
- PostgreSQL 官方文档:System Administration Functions — Backup Control Functions
- PostgreSQL 官方文档:Log-Shipping Standby Servers
- PostgreSQL 官方文档:pg_waldump
- PostgreSQL 官方文档:Monitoring Database Activity — pg_stat_archiver