侧边栏壁纸
  • 累计撰写 269 篇文章
  • 累计收到 2 条评论

PostgreSQL 误删数据怎么抢救?我配 WAL 归档与 PITR 时间点恢复踩过的几个坑

2026-11-14 / 0 评论 / 7 阅读
AI 摘要由 AI 生成

本文探讨PostgreSQL误删数据抢救方法,重点介绍了基于WAL归档与PITR的恢复机制。作者分享了在搭建和验证PITR过程中遇到的归档配置、基础备份制作、实战时间点恢复等坑点及解决方案,强调监控归档积压、正确制作基础备份和注意新旧版本参数机制变化的重要性。

上个月排查一个线上事故,研发在排查订单问题时,在生产库上把 WHERE 条件写漏了,直接把一张千万级核心流水表近两周的状态全部覆写成了初始态。

当时全站业务立刻报警。团队第一反应是找前一天凌晨两点的 pg_dump 备份。但我算了一下时间账:如果直接拿凌晨全备还原,今天白天正常跑了十几个小时的业务数据全得回滚,带来的业务损失甚至远超这几万条异常流水。

这也是很多做数据库运维的同学经常遇到的尴尬局面:全量备份只能把数据库拉回昨天的固定时间点,中间产生的数据全部蒸发。要想把数据精确抢救到“误操作发生的前一秒”,PostgreSQL 必须依靠基础全备加上 WAL 日志的连续归档,做基于时间点的恢复(PITR,Point-In-Time Recovery)。

我在搭建和验证这套 PITR 恢复机制时,前后踩过几个非常隐蔽的坑,这里把实操细节和排障记录理清楚。

归档配置踩坑:archive_command 只要阻塞,磁盘就会被迅速撑爆

配置 WAL 归档的核心原理很简单:每当 PostgreSQL 写满一个 16MB 的 WAL 段文件,后台进程就会调用 archive_command 把这个日志文件拷到归档存储(比如挂载的 NAS 或对象存储)里。

基本配置在 postgresql.conf 中:

wal_level = replica
archive_mode = on
archive_command = 'test ! -f /mnt/pg_archive/%f && cp %p /mnt/pg_archive/%f'

很多人照着官方文档写完这行配置,就以为万事大吉了。我在压测时碰到了第一个严重事故:由于内网 NFS 抖动导致挂载点卡死只读,cp 命令连续返回非零退出码。

PostgreSQL 的机制非常严格:只要 archive_command 返回失败,系统就会认为归档未完成,对应的 WAL 文件绝对不会被清理或回收。这本来是保护数据不丢的设计,但在高并发写入场景下,pg_wal 目录下的日志每分钟都在暴增。短短两个小时,服务器剩余的 80GB 磁盘被活活撑爆,数据库直接 crash 并拒绝启动。

踩了这个教训之后,我的生产环境调整了两处硬性防御:

  1. 必须引入归档积压监控:在 Prometheus 里采集 pg_stat_archiver 的指标。重点监控 archived_count 与 failed_count,同时写一个探针比对 pg_wal 目录与归档目录的延迟文件数,一旦堆积超过 10 个 WAL 文件立即触发 P1 级告警。
  2. 归档脚本加超时控制:不要直接裸奔 cp 或 rsync,在外层包一层带有 timeout 30s 的包装脚本,或者使用 pgBackRest 等专业工具,避免归档进程无限期僵死在挂载点上。

制作基础备份:不要图省事直接拷贝运行中的数据目录

要做 PITR,手头必须有一份完好的基础备份(Base Backup)。新手最容易犯的错误就是直接在数据库运行时去 rsync 数据目录。这种拷贝出来的文件往往处于数据块不一致的断裂状态,恢复时根本无法正常重放日志。

正确的做法是使用官方工具 pg_basebackup:

pg_basebackup -h 127.0.0.1 -U repl_user -D /mnt/backup/base_20261114 -Ft -z -P

参数解释:

  • -Ft 输出为 tar 包格式;
  • -z 开启 gzip 压缩;
  • -P 打印备份进度。

这里有个关键坑:如果主库写并发极高,建议加上 -X stream 参数,让 pg_basebackup 在抓取全量数据的同时另起一个连接把备份期间新生成的 WAL 也拉下来。否则一旦全备做完,主库由于日志轮转把备份起始点的 WAL 清理掉了,这份全备就直接作废了。

实战时间点恢复:新旧版本参数机制变化

假设线上事故发生在 2026-11-14 15:30:25,目标是把数据库恢复到 15:30:20。

从 PostgreSQL 12 开始,官方废弃了以前独立的 recovery.conf 配置文件,全部改由在数据目录下放置信号文件并在主配置文件里指定恢复目标。

第一步:准备干净的数据目录

停掉目标实例,将原数据目录重命名保存现场,然后解压基础全备:

systemctl stop postgresql-16
mv /var/lib/postgresql/16/main /var/lib/postgresql/16/main_broken
mkdir -p /var/lib/postgresql/16/main
tar -xzvf /mnt/backup/base_20261114/base.tar.gz -C /var/lib/postgresql/16/main/

记得检查目录权限:

chown -R postgres:postgres /var/lib/postgresql/16/main
chmod 700 /var/lib/postgresql/16/main

第二步:创建 recovery.signal 并配置 restore_command

在数据根目录下创建一个空的 recovery.signal 文件,告诉 PostgreSQL 本次启动进入恢复模式:

touch /var/lib/postgresql/16/main/recovery.signal

然后在 postgresql.conf 末尾追加恢复规则:

restore_command = 'cp /mnt/pg_archive/%f %p'
recovery_target_time = '2026-11-14 15:30:20+08'
recovery_target_action = 'pause'

注意这里有两个致命细节:

  1. 时区后缀必须显式指定:recovery_target_time 写成 '2026-11-14 15:30:20' 不带时区的话,PostgreSQL 会默认取当前操作系统或数据库参数的时区。一旦部署在 UTC 服务器上而误操作时间是按北京时间记录的,直接多回滚 8 个小时,业务数据大面积缺失。务必带上 +08。
  2. 强烈建议设置 recovery_target_action = 'pause':
    这个参数会让数据库重放到指定时间点后自动暂停并打开只读查询。运维同学可以用 psql 连上去查验那张误更新的表,确认数据是否正好停留在事故前一秒。确认无误后再执行 SELECT pg_wal_replay_resume(); 让实例正式提升为主库。如果直接写成 promote,一旦时间算错,数据库立刻生成新的时间线,想再调整时间点就得从头再解压一次几十上百 G 的全备。

恢复完成后的时间线(Timeline)与后续运维

当日志重放完毕并解除 pause 状态后,PostgreSQL 会从原有的时间线(比如 Timeline 1)自动分叉出新的时间线(Timeline 2),并在日志中输出:

LOG:  restored log file "000000010000002A00000018" from archive
LOG:  recovery stopping at restore point "2026-11-14 15:30:20+08"
LOG:  selected new timeline ID: 2
LOG:  archive recovery complete

此时有两件事必须立刻处理:

  1. 旧归档目录不能直接混用:因为主库已经切换到新时间线,新的 WAL 文件前缀会从 00000001... 变成 00000002...。如果在恢复测试机上操作,千万别把主库的归档目录当成可写目录直接覆盖,否则可能污染生产环境的连续日志流。
  2. 立刻执行一次新的全量备份:恢复上线完成之后,现有的时间线已经发生漂移,旧的基础全备无法跨时间线为后续的 WAL 做基线支撑。此时最稳妥的操作就是马上再跑一次 pg_basebackup,重置全备与归档基线。

数据库出现误操作时,人容易慌乱。平时在测试机多演练几遍这套基于全备加 WAL 的 PITR 流程,真正遇到突发误删事故时,才能在半小时内把数据稳稳当当地捞回来。

评论一下?

OωO
取消