
凌晨一点半被电话叫醒这种体验做过数据库运维的人都懂。客户的订单表被一条漏了 WHERE 的 UPDATE 扫掉了几十万行等业务方反应过来新数据又跑进去了二十分钟。老板在群里问能不能回到出事前那一分钟答案是能但前提是 PostgreSQL 提前配好了时间点恢复PITR的整套链路基础备份、WAL 归档和恢复目标。这篇把从零配置 WAL 归档、制作基础备份到真正执行一次 PITR 的完整过程写下来也把我在真实故障和多次恢复演练里踩过的坑一并讲清楚。它主要解决一个核心问题完整备份只能代表“某一天的零点”WAL 归档却能让你把数据库重放到任意一个精确时刻。适合正在做 PostgreSQL 运维、还没搭好 PITR或者搭好了但从没真正演练过恢复的人参考。1. 先搞清楚 PITR 到底在干什么WAL 是把数据库带回过去的胶水1.1 完整备份只代表“那一刻”WAL 才是持续记录变化的时间轴很多人做备份的思路是每天凌晨用 pg_dump 或者文件系统快照把数据库拷一份走。这个思路本身没错但它有个天然短板——完整备份是一个静态快照恢复出来的永远是“那个时刻”的数据库。假如故障发生在下午三点而备份是凌晨两点做的中间十三小时的数据只能靠日志重放补回来。PostgreSQL 的 WALWrite-Ahead Log就是这十三小时的胶水。数据库在做任何变更之前先把变更记录写到 WAL 日志里然后再去改数据文件和索引。WAL 文件默认 16MB 一个连续不断内容就是一条一条对数据页面的修改记录。正因为有这个机制PostgreSQL 才能在崩溃后重放日志恢复一致性也才能让我们把数据库“重放”到过去某个时间点。你可以把完整备份理解成一张旧照片WAL 理解成一段持续录像。照片不够新没关系只要录像从拍照那一刻起就没有中断你就能从照片出发按时间轴一帧一帧重放到出事前的一秒。1.2 一个漏了 WHERE 的 UPDATE为什么连数据库的回滚机制也救不了你我见过很多开发同学的第一反应是出事了赶紧回滚事务。但回滚机制只对“还没提交”的事务有效。一条 UPDATE 如果已经提交它的改动就在数据文件里落地了之后的 SELECT、索引更新、归档备份全都基于这份错误数据继续运行。等 DBA 接到报警电话时错误的提交可能已经成了过去二十分钟的主线历史。假设你有一张余额表某个时间点一条UPDATE t SET balance 0;把全表余额清了。这条 UPDATE 在 14:30:05 提交之后业务继续写入新的订单、新的对账记录。你不可能用事务回滚去撤销它因为这条事务早就结束了后续事务都建立在它的结果之上。你能做的只有让整库回到 14:30:04 那一种状态然后再决定这个时段之后的数据怎么办。这正是 PITR 的核心价值它不靠“撤销”靠的是“重放”。先载入一份基础备份再把 WAL 按顺序重放到你指定的时间点数据库就回到了那台时间机器上精准的一格。1.3 PITR 的全链路基础备份 归档 WAL 恢复目标三要素一次完整的时间点恢复需要三个要素同时满足第一是基础备份。它提供时间轴的原点通常在每天凌晨完成可以使用 pg_basebackup 生成物理一致的数据目录副本。第二是连续归档的 WAL 日志。从基础备份完成那一刻起所有 WAL 文件都要被复制到一个独立的地方比如本地磁盘、NAS 或对象存储。第三是恢复目标。你告诉 PostgreSQL 要在哪个时间点、哪个事务 ID 或 LSN 位置停下来。三者缺一个PITR 就做不成。没有基础备份起始点不存在没有归档 WAL中间一段时间无法重放没有恢复目标数据库只会一路重放到 WAL 末尾那不是“回到过去”而是“追到未来”。另外PostgreSQL 的时间线机制也值得理解。每次从备份恢复并产生新的写入都会形成一条新的时间线分支。如果恢复后发现问题想回到更早的点再走一条路可以用recovery_target_timeline latest自动选择最新时间线也可以手工指定 timeline ID。演练排错时这个参数很有用我在后面恢复步骤里会再提。2. 没有归档链路就别谈 PITRwal_level 与 archive_command 的配置细节2.1 先确认 PostgreSQL 版本别把 recovery.conf 和 signal 文件搞混配置 PITR 之前第一件事是弄清楚自己在跑哪个版本。PostgreSQL 的恢复配置在 12 版本有一次比较大的变化网上很多旧教程还在让你改 recovery.conf拿到新版本上根本不会生效。我建议在开发机上先执行两条命令postgres --version psql -c SHOW server_version;版本不同后面三个关键点不一样我用一张表直接列出来PostgreSQL 版本恢复配置文件开启恢复的方式WAL 切换函数手工备份函数9.6 及更早recovery.confrecovery.conf 存在即恢复pg_switch_xlog()pg_start_backup() / pg_stop_backup()10 / 11recovery.confrecovery.conf 存在即恢复pg_switch_wal()pg_start_backup() / pg_stop_backup()12 及以上postgresql.conf数据目录放空文件 recovery.signalpg_switch_wal()pg_start_backup() / pg_stop_backup()15 及以上postgresql.conf数据目录放空文件 recovery.signalpg_switch_wal()pg_backup_start() / pg_backup_stop()如果你的数据库还在 9.6 以下我强烈建议先升级再说。老版本不是不能做 PITR而是安全修复早已停止你花一晚上把恢复脚本调好了一个漏洞可能让前面的工作全部白费。新环境的选型我也倾向于用当前还在长期维护的主流版本而不是追最新小版本避免刚发布的小版本里隐藏的兼容性问题影响生产恢复。2.2 wal_level、archive_mode、archive_command 的正确姿势PITR 依赖归档 WAL所以三个参数必须正确设置。它们都可以放在 postgresql.conf 里# 放在 postgresql.conf wal_level replica archive_mode on archive_command test ! -f /backup/pg_wal_archive/%f cp %p /backup/pg_wal_archive/%f为什么wal_level要设成replica因为它让 PostgreSQL 记录足够完整的 WAL 信息来支持归档和流复制。minimal模式下很多操作只写少量 WAL不够重建数据页PITR 无从谈起logical虽然也够但会额外记录逻辑解码所需的信息磁盘和 WAL 开销更大。没有逻辑复制需求时replica是最合适的。archive_mode on是开启归档的开关。archive_command则定义了一个 WAL 文件写满并切换后要被送到哪里去。其中有两个占位符%p是 WAL 的原始完整路径%f只是文件名。例子里我先用test判断归档目录是否已经有同名文件防止覆盖再把文件复制到/backup/pg_wal_archive/。这个目录建议和数据库实例放在不同磁盘或者不同机器上不然数据库所在磁盘坏了归档也跟着完蛋。生产环境里也可以直接把 WAL 推到远程或对象存储archive_command 换成对应的上传命令即可。但要注意archive_command 必须能快速返回成功或失败而且必须在失败时返回非零状态PostgreSQL 才会保留这个 WAL 并反复重试归档。如果写得含糊比如把归档工具挂在后台执行PostgreSQL 可能以为归档成功了实际上文件根本没有落地。2.3 用 pg_switch_wal 和 pg_stat_archiver 验证归档是否真的落盘配置改完后archive_mode和wal_level都需要重启实例才生效archive_command可以用 reload 方式加载。重启这件事容易漏我见过有人改完参数直接等了三天一看 pg_stat_archiver 才发现归档从未启动。验证归档链路最直接的方式是手动切换一次 WAL 文件SELECT pg_switch_wal();执行后 PostgreSQL 会切出一个新的 WAL 文件并触发 archive_command。如果归档正常几秒钟后你就能在归档目录里看到这个文件。然后查一下归档状态SELECT archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time FROM pg_stat_archiver;这张视图会告诉你历史上归档成功了多少个、最后一个归档的是哪个 WAL、失败了多少次。如果failed_count在持续增长说明 archive_command 有问题一定要先解决再继续后面的工作。2.4 archive_command 常见错误权限、路径、远端存储归档命令最容易翻车的有三类。第一类是权限问题PostgreSQL 进程执行 archive_command 时用的是 postgres 系统用户归档目录如果只有 root 或 dba 账号能写命令就会一直失败。第二类是路径写错导致 WAL 文件“看似复制成功”但实际目录不对恢复时找不到文件。第三类是远端存储不稳定网络抖动、认证过期都会让归档链路中断。我在归档命令中习惯把目标路径写绝对路径并且先把目录手动chown给 postgres 用户。对远端存储需要额外考虑是否值得加一个本地盘做缓冲。比较稳妥的结构是在归档端部署一个持续接收 WAL 的服务archive_command 只负责把文件推到本机缓冲目录再由服务同步到远端避免数据库进程被网络问题阻塞。3. 基础备份怎么做才靠谱pg_basebackup 与备份留存策略3.1 pg_basebackup 命令和常用参数解读WAL 归档配好之后接下来要产生一个基础备份。PostgreSQL 官方提供的pg_basebackup是物理备份的标准工具它通过复制协议把整个数据目录打包到另一台机器或本地目录。先创建一个用于备份的账号CREATE ROLE repl_user WITH REPLICATION LOGIN PASSWORD StrongPassword;在 pg_hba.conf 里允许该账号从备份主机连接host replication repl_user 192.168.1.0/24 md5然后执行pg_basebackup -h 192.168.1.10 -p 5432 \ -U repl_user \ -D /backup/base_$(date %Y%m%d_%H%M%S) \ -Fp -Xs -P几个参数解释一下-Fp表示输出为普通目录格式恢复时直接可用如果你想打包归档可以用-Ft生成 tar 包。-Xs表示通过 WAL 流方式把备份过程中产生的 WAL 一并取走保证备份目录是自洽的。如果不用-Xs备份期间产生的 WAL 可能没进备份目录恢复时容易缺一段。-P会显示进度。-R这个参数我也经常看到但它会把备份配置成 standby 并写入连接信息PITR 基础备份不需要它用了反而可能在启动恢复时多出standby.signal的干扰。3.2 手动 start/stop backup 的兼容写法如果你用的是老版本或者因为某种原因不能用 pg_basebackup也可以用手动方式制作基础备份SELECT pg_backup_start(manual_backup_20250315);用一个独立会话执行这条之后在另一个终端里把整个数据目录复制走tar -czf /backup/base_20250315.tar.gz /data/pgdata复制完成后回到刚才的会话执行SELECT pg_backup_stop();在 PostgreSQL 15 及以上函数名改成了pg_backup_start()和pg_backup_stop()。手动方式的问题在于它不会自动处理备份期间产生的 WAL你需要在停止备份后立刻确认归档链路跟上。所以我始终推荐优先使用 pg_basebackup。3.3 定时备份与保留策略别让归档把磁盘撑爆基础备份不是做一次就完事。我习惯每天凌晨用 crontab 跑一次 pg_basebackup并保留最近七天的日备份、最近四周的周备份。WAL 归档则按业务RPO要求保留通常一到三个月。归档文件虽然单个只有 16MB但一个写库非常频繁的系统每天可能产生几十上百个 WAL积少成多很恐怖。保留策略要防止两种悲剧一是归档太少PITR 能追溯的时间太短二是归档太多备份目录满了后新的 WAL 归档不进去整条链路失效。建议用一个脚本定期清理过期备份和 WAL同时监控归档目录的使用率超过 80% 就告警。4. 恢复到指定时间点的完整步骤从数据目录到 recovery.signal4.1 准备独立目录别污染原实例现在到了真正执行恢复的时候。我强烈建议不要在原数据目录上直接动手而是把基础备份解压到一个新的目录中。原因很简单你只有一次原实例如果在恢复过程中改坏了就再也没有第二次机会了。假设基础备份存放在/backup/base_20250315目标恢复目录是/data/pg_restoresudo -u postgres mkdir -p /data/pg_restore sudo -u postgres cp -a /backup/base_20250315/. /data/pg_restore/ sudo -u postgres chmod 700 /data/pg_restore如果用-Ft打了 tar 包就先解包。备份目录里的文件权限尽量保持原样必要时把整个目录chown给 postgres 用户。恢复实例最好用独立端口比如 5433避免和原实例的端口冲突。可以在 postgresql.conf 里把port 5433改掉再加一行listen_addresses 127.0.0.1让恢复实例只在本地测试。4.2 PostgreSQL 12 之后的 signal 文件和 11 之前的 recovery.conf版本差异是这里最大的坑。PostgreSQL 12 及以上你需要在恢复用的数据目录里创建一个空文件recovery.signal然后在 postgresql.conf 里写恢复参数。PostgreSQL 11 及更早则是创建一个 recovery.conf 文件把恢复参数放进去。对于 12 版本创建信号文件sudo -u postgres touch /data/pg_restore/recovery.signal千万别误创建成standby.signal那个是给流复制备机用的创建错了数据库会一直保持备机状态而不是做时间点恢复。对于 11 及更早版本没有 signal 文件这回事直接在数据目录创建 recovery.confrestore_command cp /backup/pg_wal_archive/%f %p recovery_target_time 2025-03-15 14:30:0408 recovery_target_timeline latest后面的配置12 写在 postgresql.conf11- 写在 recovery.conf核心参数一致。4.3 设置 restore_command 和 recovery_target_time恢复时最重要的两个参数是restore_command和recovery_target_time。restore_command告诉 PostgreSQL 如何从归档目录取回 WAL 文件。我在本地目录恢复时通常这样写restore_command test -f /backup/pg_wal_archive/%f cp /backup/pg_wal_archive/%f %p这里用test -f先检查归档是否存在存在才复制如果某个 WAL 段确实不存在命令返回非零状态PostgreSQL 会知道这段日志取不到不会误当成复制成功。恢复目标则按你想回到的时间点来写。比如业务错误发生在 2025-03-15 14:30:05我建议把目标设在 14:30:04也就是错误提交之前的时刻recovery_target_time 2025-03-15 14:30:0408 recovery_target_timeline latest时间字符串必须带上时区偏移。08代表北京时间千万不要写成一个不带时区的裸时间。这个我后面会专门吐槽。如果你对精确时间没有把握也可以用 LSN 或事务 ID 作为目标比如recovery_target_lsn、recovery_target_xid。但日常出错时最容易获取的还是“错误操作大概发生在几点”所以recovery_target_time用得最多。4.4 启动实例、观察日志、确认恢复地点配置完成后以 postgres 用户启动恢复实例sudo -u postgres pg_ctl -D /data/pg_restore -l /tmp/pg_restore.log start启动后马上看日志恢复过程会输出类似下面的内容LOG: database system was interrupted; last known up at 2025-03-15 14:00:0008 LOG: starting point-in-time recovery to 2025-03-15 14:30:0408 LOG: restored log file 0000000100000000000000A0 from archive ... LOG: recovery stopping before commit of transaction 123456, time 2025-03-15 14:30:0408 LOG: recovery has paused“recovery stopping before commit of transaction”说明它找到了候选的目标位置并停在那里。此时实例已经可以接受只读连接但还处于恢复模式。用下面这条 SQL 验证状态SELECT pg_is_in_recovery(), pg_last_xact_replay_timestamp();第一列返回true说明实例仍在恢复状态第二列显示最后一条重放事务的时间戳它应该接近你设定的目标时间。再去业务表中数一数行数确认关键数据已经回来了。4.5 验证数据完整性并切换成可写实例查询正确并不代表可以立刻投入使用。我建议至少检查三样东西关键业务表的行数、最近一段时间是否有异常数据、应用日志中依赖的连接信息是否指向这个新实例。确认无误后如果这个恢复实例就是你要对外提供服务的新主库需要把它切换成正常可写状态sudo -u postgres pg_ctl -D /data/pg_restore stop sudo -u postgres rm /data/pg_restore/recovery.signal sudo -u postgres pg_ctl -D /data/pg_restore -l /tmp/pg_restore.log start删掉recovery.signal后实例会以普通主库模式启动不再处于恢复模式可以正常写入。此时再执行SELECT pg_is_in_recovery();应当返回false。5. 恢复排错实录时区、事务边界和 WAL 缺口5.1 时区问题时间格式必须带时区我第一次在真实环境做时间点恢复就栽在时区上。当时用户说错误操作发生在下午两点半我在 recovery_target_time 里写了一个2025-03-15 14:30:04没带时区。结果恢复出来的数据差了八个小时业务表里全是下午三点多甚至四点的状态。原因很简单recovery_target_time 接收的是带时区的 timestamp 类型。不带时区时PostgreSQL 会按服务器所在时区解释而数据库时间戳在 WAL 内部通常以 UTC 记录。如果你的服务器是北京时间裸时间会被当作北京时间解析偏移对不上恢复点就偏了。正确写法应该明确写偏移recovery_target_time 2025-03-15 14:30:0408或者统一转成 UTCrecovery_target_time 2025-03-15 06:30:0400实际做恢复时我先用date -d 2025-03-15 14:30:04 %F_%T%:z把我要的目标时间转换成带时区的字符串然后再填进配置这样能减少一次人为换算错误。5.2 恢复目标应该定在“错误发生前”而不是“错误事务内”另一个常见问题是目标时间卡得太死。很多人直接拿错误事务的提交时间当目标比如错误 UPDATE 在 14:30:05 提交就把目标设在 14:30:05。这会导致两种结果要么恢复正好把错误事务包含进来数据依旧不对要么因为 inclusive 参数的边界语义恢复停在了一个你不太确定的位置。我的经验是把目标定在已知业务正常的最后一个时间点并且尽量留出一两秒缓冲。比如确认错误是在 14:30:00 之后开始的那目标就定 14:29:59 或者更早一点。宁可少包含一秒正常事务也不要侥幸跨过错误事务。如果不得不在错误事务边界上做文章可以加一行recovery_target_inclusive false它表示“停在目标时间对应事务之前”。但说实话日常故障里你很难精确到“那一秒内到底提交了几个事务”所以提前两秒永远比卡边界安全。5.3 缺 WAL 日志时怎么办能往回退但别硬撑恢复时最绝望的错误信息是日志里出现requested WAL segment 0000000100000000000000A1 has already been removed或者could not open file ... No such file or directory。这说明归档链路有缺口某些 WAL 段没有完整归档或已被清理。遇到这种情况第一步是把日志里报缺的 WAL 文件名和目录里的文件比对一遍确认缺失范围。如果缺口发生在恢复目标之后说明你其实不需要那一段可以考虑把目标提前到缺口之前再试一次。如果缺口发生在恢复目标之前那段 WAL 是必须的PITR 在这个时间点无法继续。坦白讲缺 WAL 时不存在什么魔法补救。唯一能缩小损失的办法是把目标前移到归档完整的最后一个时间点接受丢失一部分数据然后让业务补录。所以我们配置归档监控时才要那么较真因为你根本不知道哪一天的archive_command悄悄失败了。5.4 误以为“恢复完成”但数据库提示仍在恢复模式还有一次同事做恢复演练日志已经显示恢复了但应用连上去发现不能写业务直接报“cannot execute INSERT in a read-only transaction”。他以为恢复失败了差点重置目录重新来。其实这只是因为实例还停留在恢复模式。PITR 的目标是让数据库停在某个历史时间点在删除recovery.signal之前它会保持只读恢复状态。这本身是正常的。要变成可写主库就得执行 4.5 节里那几步先停库、删掉recovery.signal、再启动。相反如果你用pg_is_in_recovery()查到true又没设置任何恢复目标那多半是误建了standby.signal或者配置文件里残留了备机参数。可以打开数据目录检查 signal 文件不要盲目删。6. 把 PITR 变成日常能力定期演练、归档监控与最后提醒6.1 定期做恢复演练别等出事后第一次恢复没有任何一个 DBA 想在生产事故中第一次练习恢复。所以我强烈建议把“月度恢复演练”写进运维计划每月抽出一个小实例用本周的基础备份和归档 WAL恢复到一个随机时间点然后校验数据一致性。演练脚本可以做得很简单核心还是那几步解压基础备份、创建 recovery.signal、设置 restore_command 和 recovery_target_time、启动实例、检查 pg_is_in_recovery 和最后重放时间戳。跑熟了之后你会对版本差异、归档路径、时区格式这些东西形成肌肉记忆真出事时反而不会手忙脚乱。6.2 归档监控和告警的落地做法归档链路必须监控起来而不是出了问题才回头看。我每天会在巡检脚本里查这几个指标SELECT archived_count, failed_count, last_archived_time FROM pg_stat_archiver;一旦failed_count增长或者last_archived_time停留在几分钟前立刻报警。归档目录的大小和磁盘剩余空间也要纳入监控防止 WAL 把盘写满。只要归档连续、基础备份可恢复PITR 才算真正可用。6.3 还有几个值得记住的小习惯最后分享几个我自己的操作习惯。恢复目标时间我从不凭记忆写先查一次业务日志或者 Binlog 型审计日志确认错误操作对应的准确时间再转成带时区格式。基础备份生成后我喜欢立刻挑一个校验和文件记录恢复时能快速确认备份文件有没有损坏。生产环境恢复时如果原实例还在运行千万不要把恢复目录放在原数据目录的子路径里否则可能出现不可预期的文件覆盖。PITR 是一个平时看不见、关键时刻能救命的能力。它不需要每天操作但值得在某个安静的周末花两小时配好、备份好、演练一次。等真到了凌晨被电话叫醒的那一天你会感谢那两小时。