PostgreSQL 备份恢复与 PITR 时间点恢复实战
PostgreSQL 没有 MySQL 那样的物理热备工具链,它的备份是两件套:基础备份(pg_basebackup)负责某个时间点的数据快照,WAL 归档负责快照之后的所有变更。两者缺一不可——只有基础备份只能恢复到备份点,只有 WAL 没有基础备份则无从起步。本文以 PostgreSQL 15/16 为例,完整走通备份、PITR 与校验。
先说前提:必须先开启 WAL 归档(archive_mode=on),否则 PITR 做不了;归档目录要与数据目录分盘存放,归档失败会拖垮主库;恢复演练一律用独立实例,不要在生产库上练。
一、开启 WAL 归档:PITR 的地基
归档配置分三项:开启归档、指定归档命令、确认归档不丢。归档命令失败会重试,长期失败会导致 pg_wal 堆积把磁盘打满,所以归档目录的空间与监控要先到位。
# postgresql.conf(主库)
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backup/pg/wal/%f && cp %p /backup/pg/wal/%f'
archive_timeout = 3600
max_wal_senders = 5
# 重启后验证归档是否正常工作
# psql -c "SELECT pg_switch_wal();"
# ls -lh /backup/pg/wal/ | tail -5
# psql -c "SELECT * FROM pg_stat_archiver;"
检查 pg_stat_archiver 的 failed_count 是否为 0、last_archived_time 是否在更新。归档命令里的 test ! -f 是防重复归档的保护,不要删掉。
二、基础备份:pg_basebackup 的正确姿势
pg_basebackup 从主库在线拉一份一致性快照,备份期间主库正常读写。关键参数:-Ft 打包成 tar、-z 压缩、-P 显示进度、-D 指定备份目录。备份时自动包含备份开始点的 WAL 位置信息,恢复时靠它定位。
# 创建备份专用账号(只需复制权限)
# psql -c "CREATE USER bkp WITH REPLICATION LOGIN PASSWORD 'BkpPass_2026';"
mkdir -p /backup/pg/base/2026-11-09
pg_basebackup -h 127.0.0.1 -U bkp -D /backup/pg/base/2026-11-09 \
-Ft -z -P -v --wal-method=stream
# 备份完成后检查:应有 base.tar.gz 与 pg_wal.tar.gz
ls -lh /backup/pg/base/2026-11-09/
cat /backup/pg/base/2026-11-09/backup_manifest 2>/dev/null | head -20
--wal-method=stream 会把备份期间产生的 WAL 一起流式拉下来,避免备份结束时 WAL 已被回收导致备份不可用。备份目录的权限必须是 postgres 用户可读写。
三、PITR 恢复:恢复到误删之前的那一分钟
PITR 的流程:解压基础备份到新的数据目录 → 写 recovery 配置指定恢复目标时间 → 启动实例自动重放 WAL → 到达目标时间后停止恢复、正常提供服务。PostgreSQL 12+ 用 recovery.signal 文件 + postgresql.auto.conf 的恢复参数,不再用 recovery.conf。
<pre class="wp-block-syntaxhighlighter-code"># 1. 解压基础备份到恢复实例的数据目录
rm -rf /var/lib/pgsql/data-recovery/*
tar -xzf /backup/pg/base/2026-11-09/base.tar.gz -C /var/lib/pgsql/data-recovery
tar -xzf /backup/pg/base/2026-11-09/pg_wal.tar.gz -C /var/lib/pgsql/data-recovery/pg_wal
chown -R postgres:postgres /var/lib/pgsql/data-recovery
# 2. 写恢复目标:恢复到 2026-11-09 10:30 之前
cat >> /var/lib/pgsql/data-recovery/postgresql.auto.conf <<'EOF'
restore_command = 'cp /backup/pg/wal/%f %p'
recovery_target_time = '2026-11-09 10:30:00'
recovery_target_action = 'promote'
EOF
touch /var/lib/pgsql/data-recovery/recovery.signal
# 3. 启动恢复实例,观察日志确认重放到目标时间后提升为主库
# pg_ctl -D /var/lib/pgsql/data-recovery -l /tmp/recovery.log start
# tail -f /tmp/recovery.log | grep -i -E 'recovery|consistent|promote'</pre>
恢复目标时间必须落在基础备份之后、WAL 归档连续覆盖的区间内,否则实例会在 WAL 断点处停下。恢复完成后立即验证:误删的表是否回来、数据是否停在目标时间之前,再决定是否切换业务。
四、恢复后的检查与主从衔接
PITR 恢复出来的实例是一个新的时间线(timeline),旧的从库不能直接接回来,必须重建。恢复后要做三件事:验证数据、确认时间线、重建从库。
<pre class="wp-block-syntaxhighlighter-code"># 验证数据:表数量、关键表行数、误删表是否恢复
psql -h 127.0.0.1 -p 5433 -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';"
psql -h 127.0.0.1 -p 5433 -c "SELECT count(*) FROM app.orders WHERE created_at < '2026-11-09 10:30:00';"
# 确认时间线已推进
psql -h 127.0.0.1 -p 5433 -c "SELECT timeline_id FROM pg_control_checkpoint();"
# 重建从库:从恢复后的主库重新拉基础备份
pg_basebackup -h 127.0.0.1 -p 5433 -U bkp -D /var/lib/pgsql/data-standby -R -P -v</pre>
五、并行与压缩备份:大库备份提速的关键
单线程备份大库时,备份窗口经常超过业务允许的停机时间。pg_basebackup 的并行备份(-j)可以把表空间分片并行传输,配合服务端压缩能显著缩短备份时间。但并行度不是越高越好,超过磁盘与网络的承受能力反而更慢,还会挤占业务查询的 I/O。
# 并行备份:4 个并行流 + 服务端压缩,备份到带日期的目录
pg_basebackup -h 127.0.0.1 -U repl -D /backup/pg/base/2026-11-09 \
-Ft -z -j 4 -P -v -c fast
# -Ft:tar 格式输出;-z:gzip 压缩;-j 4:4 个并行流
# -c fast:尽快完成检查点,缩短备份开始时的等待
# 大库先看各表空间大小,再定并行度
psql -c "SELECT spcname, pg_size_pretty(pg_tablespace_size(oid)) FROM pg_tablespace;"
du -sh /var/lib/pgsql/data/* 2>/dev/null | sort -rh | head
# 备份耗时与速率记下来,作为以后排期的依据
# 备份完成后校验 tar 包完整性
ls -lh /backup/pg/base/2026-11-09/
tar -tzf /backup/pg/base/2026-11-09/base.tar.gz > /dev/null && echo "tar OK"
并行备份只对多表空间或大表多的库效果明显,小库用单线程反而更快。备份期间要盯着主库的磁盘 I/O 与复制延迟,备份把主库拖慢时先降并行度。压缩级别不用追求最高,gzip 默认级别在速度与体积之间最均衡,备份窗口紧张时宁可体积大一点也要快一点。
六、归档失败处理:WAL 积压是最危险的静默故障
WAL 归档失败时,主库不会立刻报错,只会把 WAL 文件越积越多,直到把 pg_wal 所在的磁盘打满、主库停写。归档监控的重点不是“有没有报错”,而是“归档是否连续、延迟是否在涨”。发现积压先保主库磁盘,再修归档链路,顺序不能反。
# 看归档状态:失败数与最后一次归档时间
psql -c "SELECT archived_count, failed_count, last_archived_time, last_failed_time FROM pg_stat_archiver;"
# 看 pg_wal 积压了多少(超过阈值先告警,不要等磁盘满)
du -sh /var/lib/pgsql/data/pg_wal
ls /var/lib/pgsql/data/pg_wal | wc -l
# 常见原因定位:归档命令本身、目标目录权限、磁盘空间
# 手动跑一次归档命令,看报错
# test ! -f /backup/pg/wal/%f && cp %p /backup/pg/wal/%f
ls -ld /backup/pg/wal && df -h /backup/pg/wal
# 应急:归档目标不可用时,先把 WAL 拷到临时大盘保住,再修归档
# mkdir -p /tmp/wal-emergency && cp /var/lib/pgsql/data/pg_wal/0000000100000000000000* /tmp/wal-emergency/
# 归档恢复后,用 pg_archivecleanup 清理已归档的旧 WAL
# pg_archivecleanup /var/lib/pgsql/data/pg_wal 0000000100000000000000A0
千万不要为了解决磁盘满而直接删 pg_wal 里的文件:删掉的 WAL 可能正是 PITR 恢复需要的,删了等于断了恢复链。正确的应急顺序是扩磁盘或挪走 WAL 到临时盘,等归档恢复后再由系统自动清理。归档失败的告警阈值建议设为“失败数 > 0 立即告警”,不要等积压到磁盘 90% 才发现。
七、主从切换后的 PITR:时间线变了,恢复目标也要变
主从切换后新主库会开启一条新的时间线(timeline),旧时间线上的 WAL 归档不再直接适用于新主库。PITR 恢复时如果目标时间点跨越了切换点,必须明确指定要恢复到哪条时间线,否则恢复会停在切换点之前、或者直接报错。切换后第一件事是确认新主库的归档链路已接通、时间线已推进。
# 看当前时间线与归档状态(切换后在新主库上确认)
psql -c "SELECT timeline_id FROM pg_control_checkpoint();"
psql -c "SELECT archived_count, failed_count FROM pg_stat_archiver;"
# 恢复到指定时间线:recovery.conf / postgresql.conf 里指定
# recovery_target_timeline = 'latest' # 跟到最新时间线
# recovery_target_timeline = '2' # 明确指定时间线 2
# recovery_target_time = '2026-11-09 10:30:00+08'
# 切换后先在新主库上做一次新的基础备份,旧备份 + 旧 WAL 链只能恢复到切换前
pg_basebackup -h 127.0.0.1 -U repl -D /backup/pg/base/2026-11-09-after-switchover -Ft -z -P -v
# 检查新备份对应的 WAL 归档是否连续
ls /backup/pg/wal/ | sort | tail -3
# 演练:用新备份 + 新时间线 WAL 做一次恢复,确认链路是通的
时间线问题最容易在“切换后第一次演练”时暴露:平时演练用的都是旧链路,切换后没人重新验证。建议把“主从切换后 24 小时内做一次 PITR 演练”写进运维手册,演练通过才算切换真正完成。保留旧时间线的 WAL 归档至少一个保留期,切换前的恢复需求可能随时出现。
八、保留期、监控与演练清单
- 保留期:基础备份至少保留 2 份,WAL 归档保留到最老的基础备份之后再加 1 天;异地再存一份,保留期到期前先确认新备份成功。
- 监控项:归档失败次数(pg_stat_archiver.failed_count)、pg_wal 目录大小、基础备份任务是否按时完成、备份大小是否异常突变。
- 演练:每月至少一次完整 PITR 演练,记录恢复耗时(RTO 真实值)与数据验证结果;演练用的恢复时间点每次换一个,避免只练固定场景。
- 避坑:归档目录与数据目录分盘;不要手动删 pg_wal 下的文件,让归档与检查点机制自己回收;恢复前先确认 WAL 归档连续无断点,断点处恢复会停下。
- 备份校验:备份后用
pg_verifybackup校验基础备份的完整性,发现损坏立即重备,不要等恢复时才发现。
# 备份完整性校验(PostgreSQL 13+)
pg_verifybackup /backup/pg/base/2026-11-09
# 归档连续性检查:看 WAL 文件名是否连续
ls /backup/pg/wal/ | sort | tail -5
# 演练后清理恢复实例,避免占用磁盘
# rm -rf /var/lib/pgsql/data-recovery /tmp/recovery.log







