做数据库运维这些年,我见过太多“备份一时爽,恢复火葬场”的场面了。PostgreSQL的备份方式,翻来覆去就是那几套:pg_dump逻辑转储、pg_basebackup物理基础备份、WAL归档配合时间点恢复,再讲究一点就上流复制从库或专业备份工具。可就是这么几个看似简单的东西,真到出问题的时候,能在10分钟内恢复的人少之又少。这篇不是给官方文档背书,而是结合我在生产环境里实操过的经验,把PostgreSQL备份从选型、命令、坑点到恢复演练一次性讲透。适合刚接手PG维护的DBA、准备把备份方案升级一下的团队,也适合那些“备份脚本一直在跑但从来没恢复过”的兄弟——看完你就明白,备份方案到底该怎么设计。
1. 备份方案设计:先回答三个问题,再选工具
很多人一上来就问“用pg_dump还是pg_basebackup”,我习惯先反问三个问题:要恢复什么?能接受丢多少数据?恢复要多快?这三个问题直接决定了备份方案的长相。
1.1 三条基准线:RPO、RTO与恢复粒度
RPO(恢复点目标)回答的是“能丢多少数据”。业务方说“最多丢5分钟”,那你就必须在WAL归档或流复制上做文章;如果业务方说“丢一天无所谓,重导一遍就行”,那每天跑一次pg_dump也说得过去。RTO(恢复时间目标)回答的是“多久能恢复”。逻辑备份恢复一个几百GB的库可能要小时级,而物理备份配合归档可以做到分钟级,前提是脚本、流程都到位。
除了RPO和RTO,我还会加一条“恢复粒度”。误删一张表、一个schema,和整个实例崩溃,诉求完全不一样。误删单表时,用逻辑备份最舒服,可以直接把那张表捞出来;实例崩溃时,物理备份是兜底,逻辑备份在这种场景下又慢又痛苦。所以成熟方案里通常两者共存:逻辑备份管“细粒度找回”,物理备份管“整体灾难恢复”。
1.2 三种备份方式的选型逻辑
我见过不少人把备份方式分成“逻辑备份”和“物理备份”两类,但在实际工程里应该分成三档:
| 方式 | 原理 | 恢复粒度 | 恢复速度 | 典型场景 |
|---|---|---|---|---|
| 逻辑备份(pg_dump) | SQL/自定义格式导出数据 | 库、schema、表级 | 慢,数据量越大越明显 | 小库、单表误删找回、跨版本迁移 |
| 物理冷备(直接拷贝数据目录) | 停库后文件拷贝 | 整个实例 | 快,但需要停服 | 低可用性要求的测试环境、临时环境 |
| 物理备份+WAL归档(pg_basebackup+PITR) | 基础备份+持续归档WAL | 整个实例,可恢复任意时间点 | 较快,且能精确到秒级恢复点 | 生产环境标准配置,支持误操作回滚 |
选择逻辑很简单:数据量在几十GB内、业务容忍度高的,逻辑备份够用了;数据量上百GB、业务不能长时间中断的,直接上“物理备份+WAL归档+从库”;至于纯物理冷备,我只能说能用,但前提是你能接受停库那段窗口,生产环境我一般不建议。
2. 逻辑备份实战:pg_dump 组合拳
pg_dump是PostgreSQL自带的老牌备份工具,别嫌它“太基础”,很多生产事故最终就是靠它救回来的。
2.1 pg_dump 核心参数,抄作业版
裸跑一条pg_dump -d mydb > mydb.sql确实能备份,但这只是入门。真正实用的是这些参数:
pg_dump -h 127.0.0.1 -p 5432 -U backup_user \ -d mydb \ -Fc -f /backup/mydb_$(date +%Y%m%d).dump-Fc太重要了。它生成的是PostgreSQL自定义格式的归档文件,不是纯文本SQL。为什么推荐它?因为只有自定义格式才能配合pg_restore做选择性恢复、并行恢复、排序恢复,这些在“误删一张表要捞数据”的场景里都是救命能力。纯SQL文本文件虽然可以直接psql灌进去,但一张表误删了根本没法定向提取。
如果只想备份某几个表或某个schema,用-t和-n过滤:
pg_dump -d mydb -n public -t orders_2024 -Fc -f /backup/orders_2024.dump注意-t和-n可以组合,但表名匹配规则比较讲究,支持通配符,生产环境我建议先在测试库跑一遍pg_dump --schema-only确认对象集合,再全量导出,避免漏表。
很多时候备份用户不是超级用户。建议单独建一个备份账号,只授予需要的权限,比如:
CREATE USER backup_user WITH PASSWORD 'xxxx'; GRANT CONNECT ON DATABASE mydb TO backup_user; GRANT USAGE ON SCHEMA public TO backup_user; GRANT SELECT ON ALL TABLES IN SCHEMA public TO backup_user;如果库里有序列、函数、触发器,还需要额外授权。图省事可以直接拿超级用户做备份,但生产环境安全规范一般不允许,我碰到过因为权限不足导致备份出来的文件不完整的情况,恢复时才发现少了好几张表,那真是血压飙升。
2.2 pg_restore:别只会 psql 一把梭
备份的最终目的是恢复,可我看到太多人只会psql -f backup.sql硬灌,完全不碰pg_restore。自定义格式必须用pg_restore,这个工具的真正价值在选择性恢复:
pg_restore -h 127.0.0.1 -U postgres \ -d mydb \ --clean --if-exists \ -t orders_2024 \ /backup/mydb_20240101.dump这条命令能把归档里的orders_2024表单独恢复到目标库,--clean --if-exists会先删除目标表再重建,避免重复执行报错。并行恢复也是它的强项:
pg_restore -j 4 -d newdb /backup/mydb_20240101.dump-j指定并行度,能大大缩短恢复时间,但前提是目标库的CPU和IO扛得住,我一般从4开始,而不是无脑拉高。
有一点必须提醒:pg_restore恢复时默认是“先结构后数据再索引”,索引是最后建的。这是好事,建索引很耗IO,放到最后能让整体恢复更稳。但如果你恢复的是生产库,恢复完成后要记得手动ANALYZE,否则优化器对统计信息一无所知,查询性能会明显变差。
2.3 pg_dumpall 与全局对象
pg_dump只导出数据库本身,不导出集群级别的角色、表空间。所以一个完整的逻辑备份方案必须包含pg_dumpall --globals-only:
pg_dumpall --globals-only -U postgres -f /backup/globals.sql这点极容易被忽略。我接过一个项目,之前的DBA只备份业务库,后来服务器迁移,新环境连角色都没建,数据导进去后一堆权限错误,光是重建角色就耗了半天。正确做法是每天的备份任务里加上这条,并在恢复时先执行globals.sql,再执行数据库级恢复。
2.4 逻辑备份的三个注意点
第一,pg_dump默认是一致性快照,不需要锁表,但它会对系统表、元数据有额外开销,大库备份时段尽量避开业务高峰。我一般安排在凌晨低峰期,并错开批处理任务。
第二,不要在备份命令里直接写明文密码。用.pgpass文件或PGPASSWORD环境变量,但PGPASSWORD在cron里会被别的进程看到,谨慎使用。我更推荐在用户目录放.pgpass:
127.0.0.1:5432:mydb:backup_user:密码然后chmod 600 ~/.pgpass,pg_dump会自动读取。
第三,备份文件要带时间戳,并且定期清理旧文件。我见过一个老哥备份脚本没问题,但从不清理,结果磁盘被备份文件塞满,数据库直接只读了,讽刺得很。
3. 物理备份与PITR:pg_basebackup + WAL归档
如果你的业务已经跑到上百GB,还指望每天pg_dump全量备份,那恢复时间会让人抓狂。这时候必须用物理备份,而且要和WAL归档配合,做成时间点恢复(PITR)。
3.1 没有WAL归档,basebackup只是半个方案
先理清概念。pg_basebackup做的是基础备份,它拷贝的是某个一致性检查点时刻的数据目录。如果只有这一份基础备份,你只能恢复到“备份完成那一刻”,之后的数据全丢。要实现“恢复到任意时间点”,必须配合WAL归档。
WAL就是PostgreSQL的预写日志,记录了所有数据变更。基础备份加上从备份点到目标时间的WAL归档,就是完整的时间点恢复能力。用生活类比,基础备份是一张照片,WAL是一段录像,PITR能让你把录像倒到任意一帧。
开启归档需要调整postgresql.conf:
wal_level = replica archive_mode = on archive_command = 'test ! -f /backup/archive/%f && cp %p /backup/archive/%f'wal_level至少是replica,如果要用逻辑复制则设为logical。archive_command里的%p是源WAL路径,%f是文件名。前面加test ! -f是为了避免归档重名覆盖,这也是官方文档的建议。
改完参数要重启实例,或者执行SELECT pg_reload_conf()。这里有个常见误解:archive_mode和archive_command改了之后,wal_level必须重启才能生效,不是reload就行的,具体用pg_ctl restart或重启服务。
我用的是本地目录归档,生产环境更建议归档到独立的备份服务器或对象存储,避免和数据库同盘故障。归档目录要做定期清理,保留周期取决于你的恢复窗口和磁盘成本,我一般保留最近7天WAL加上每日全量备份,总共能覆盖近几天的任意时间点。
3.2 pg_basebackup 实测步骤
先确认主库给了备份用户合适的权限,通常需要REPLICATION权限:
CREATE USER backup_user WITH REPLICATION PASSWORD 'xxxx';然后在从库或应用服务器上执行:
pg_basebackup -h 主库IP -p 5432 -U backup_user \ -D /backup/base/$(date +%Y%m%d) \ -Fp -Xs -P -R参数拆解:
-Fp:输出为普通文件格式,比-Ft的tar格式更适合直接当数据目录用。-Xs:流式复制WAL,不在本地生成pg_wal临时文件,速度更快,生产环境用这个。-P:显示进度。-R:自动生成standby.signal,这在搭建从库时特别有用,后面会讲。
执行完检查备份目录是否完整,尤其看看backup_label和tablespace_map是否存在,这两个文件是基础备份的身份标识。
备份完成后,我建议在那个备份目录上再跑一次校验:
/usr/pgsql-16/bin/pg_controldata /backup/base/20240101如果能正常输出,说明备份目录的基本结构没问题。
3.3 一次完整的时间点恢复演练
纸上谈兵没意思,直接走一遍PITR恢复流程。
前提是你有一份基础备份/backup/base/20240101,以及从那一刻开始到目标时间点的完整WAL归档,归档目录在/backup/archive/。
第一步,把基础备份放到新数据目录,比如/var/lib/pgsql/16/data。注意目录权限必须是postgres用户可读写,否则启动会报错。
cp -a /backup/base/20240101/* /var/lib/pgsql/16/data/ chown -R postgres:postgres /var/lib/pgsql/16/data/第二步,创建恢复信号文件。PostgreSQL 12及以后用recovery.signal,12之前在数据目录放recovery.conf:
touch /var/lib/pgsql/16/data/recovery.signal第三步,配置恢复目标。在postgresql.conf或postgresql.auto.conf里加:
restore_command = 'cp /backup/archive/%f %p' recovery_target_time = '2024-01-01 03:30:00' recovery_target_inclusive = true recovery_target_action = pauserecovery_target_time写你要回滚到的时刻,比如业务误删数据发生前的时间点。pause的意思是达到目标后暂停恢复,先别急着写库,等人工确认数据没问题。
第四步,启动实例并观察日志:
pg_ctl -D /var/lib/pgsql/16/data start启动后实例会进入恢复模式,持续应用WAL。查看日志确认“recovery stopping before commit of transaction”之类的信息,说明已经到了目标点。此时数据库是只读状态,可以查询验证数据。
第五步,确认一切正常后,结束恢复并提升为主库:
SELECT pg_promote();执行完数据库变成可读写。如果检查发现恢复目标不对,别急着promote,停下来重新调整参数再来一次。
这个流程我第一次跑的时候花了近两个小时才搞清楚,原因是restore_command里%p的路径没匹配到归档目录结构,一直报“could not open file”。后来才明白,%p是PostgreSQL期望的目标路径,%f才是归档文件名,两个变量对应关系不能搞反。
3.4 初始化参数自查:避免新装环境挖坑
很多“安装教程”不会讲清楚备份相关参数,等你真正要做PITR时才发现archive_mode还是off,wal_level还是minimal,那就只能推倒重来。
新装PostgreSQL之后,我建议第一时间检查这几项:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| wal_level | replica | 默认就是replica,但别手动降到minimal |
| archive_mode | on | 默认off,必须改 |
| archive_command | 归档命令 | 提前配好,并测试命令可执行 |
| max_wal_senders | 10左右 | 给从库和pg_basebackup预留连接 |
| hot_standby | on | 从库上允许只读查询,用于备份和分流 |
这几个参数里,wal_level修改需要重启,max_wal_senders也需要重启,所以最好在数据库初始化阶段就定好,别上线后再折腾。
3.5 记住3-2-1原则
无论用什么工具,备份的基本盘是3-2-1:至少3份数据拷贝,2种不同的存储介质,1份存到异地或至少独立于生产环境的地方。
我见过不少团队,备份文件和数据库同在一块物理盘上,硬盘一坏,备份全没,这是最典型的“备份了个寂寞”。哪怕只是每周把备份文件同步到另一台机器,也能规避很大一部分风险。
4. 自动化备份与从库:把“备份”变成系统能力
手动备份能跑通,但生产环境不能依赖人肉。真正的备份方案应该是自动化的,最好还能利用从库来分摊压力。
4.1 流复制从库:备份的天然帮手
PostgreSQL的流复制从库不只是高可用组件,它同时也是备份的好帮手。在从库上跑逻辑备份,不影响主库性能;在从库上做物理备份,也能得到一份一致性拷贝,因为从库本身就在持续应用WAL。
搭建从库的经典流程:
第一步,主库配置:
wal_level = replica max_wal_senders = 10 hot_standby = on同时在pg_hba.conf允许备份用户从从库IP发起复制连接:
host replication backup_user 192.168.1.0/24 md5第二步,在从库机器上执行:
pg_basebackup -h 主库IP -U backup_user \ -D /var/lib/pgsql/16/data \ -Fp -Xs -P -R-R会自动生成standby.signal和主库连接信息primary_conninfo,重启从库实例后,它就是流复制备库了。
第三步,验证同步状态:
SELECT client_addr, state, sync_state, replay_lsn FROM pg_stat_replication;看到state=streaming就是正常的。
有了从库,备份任务可以这么安排:
- 主库负责生产业务和WAL归档。
- 从库负责日常pg_dump逻辑备份,避开主库IO消耗。
- 定期在从库上做一次pg_basebackup物理备份,作为灾难恢复兜底。
要注意从库的WAL保留问题。如果从库跟不上,主库的WAL可能被清理,导致从库中断,这时能从pg_stat_replication看到replay_lsn落后太多。解决方法是定期检查延迟,并给从库设置合理的wal_keep_size或使用复制槽。
4.2 备份脚本模板:把命令变成服务
我这边分享一个生产环境里用过的逻辑备份脚本,核心是一个bash脚本加crontab,简单可靠。
#!/usr/bin/env bash set -euo pipefail BACKUP_DIR="/data/backups/pg_dump" KEEP_DAYS=7 DB_HOST="127.0.0.1" DB_PORT="5432" DB_NAME="mydb" LOG_FILE="/var/log/pg_backup.log" mkdir -p "${BACKUP_DIR}" TIMESTAMP=$(date +%Y%m%d_%H%M%S) OUT_FILE="${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump" if pg_dump -h "${DB_HOST}" -p "${DB_PORT}" -U backup_user \ -d "${DB_NAME}" -Fc -f "${OUT_FILE}"; then echo "$(date +'%F %T') backup ok ${OUT_FILE}" >> "${LOG_FILE}" else echo "$(date +'%F %T') backup failed" >> "${LOG_FILE}" exit 1 fi # 校验归档可读性 pg_restore -l "${OUT_FILE}" >/dev/null 2>&1 || { echo "$(date +'%F %T') archive corrupt: ${OUT_FILE}" >> "${LOG_FILE}" exit 1 } # 清理旧备份 find "${BACKUP_DIR}" -name "*.dump" -mtime +"${KEEP_DAYS}" -delete脚本里两点很关键:
一是set -euo pipefail。没有这个,pg_dump失败时脚本可能继续往下跑,给你生成一个空文件,备份日志还显示成功,这是最阴的坑。
二是pg_restore -l校验归档文件能否被正确读取。这能提前发现文件损坏,而不是等到恢复时才炸。
crontab配置:
30 2 * * * /usr/local/bin/pg_backup.sh >> /var/log/pg_backup_cron.log 2>&1这里有个细节:cron环境变量和交互式终端不同,脚本里的命令建议用绝对路径,PATH也在脚本开头重新设置,否则很可能出现手动执行正常、cron执行报错的玄学问题。
4.3 每季度一次的恢复演练
说句不中听的,很多团队的备份方案从上线起就没真正恢复过。文件每天都在增长,日志显示success,但没人知道恢复起来需要多久、会不会报错。
我从一个事故里学到教训。当时磁盘坏道导致备份文件损坏,等要恢复时才发现一堆文件打不开,只能连夜从磁盘镜像里捞数据。从那以后,我强制规定每季度至少做一次恢复演练:把最新备份恢复到一台临时实例,跑几个核心查询,验证数据一致性和恢复时长。演练结果要记录,超过RTO目标就要调整方案。
这也是为什么我在脚本里坚持做pg_restore -l校验的原因,它虽然不能100%证明数据能恢复,但至少能证明归档文件的基本完整性。
5. 企业级工具与跨库经验参考
如果你管理的库不止一两个,而是几十几百个,手动脚本可能已经不够用了。这时候可以看看专业的备份工具,它们解决的痛点是:全量、增量、压缩、保留策略、远程备份,这些东西靠手搓都可以,但维护成本很高。
5.1 pgBackRest:更省心的企业级选择
pgBackRest是PostgreSQL生态里很成熟的开源备份工具,它最吸引我的是增量差量备份和存储端压缩加密。
配置文件/etc/pgbackrest.conf大致长这样:
[global] repo1-path=/backup/pgbackrest repo1-retention-full=7 process-max=4 log-level-console=warn [mycluster] pg1-path=/var/lib/pgsql/16/data创建备份集并做第一次全量备份:
pgbackrest --stanza=mycluster --type=full backup之后的增量备份:
pgbackrest --stanza=mycluster --type=incr backup恢复命令:
pgbackrest --stanza=mycluster --type=time --target="2024-01-01 03:30:00" restore用pgBackRest最大的好处是,它把基础备份和WAL归档管理统一起来,不用自己写归档命令和清理逻辑。它还天然支持并行、压缩和断点续传,对跨机房、跨地域备份场景很有用。
5.2 Barman 与其它组合
Barman是另一款很有名的PG备份管理工具,命令风格更偏向“服务化”,安装后通过barman backup、barman recover来操作。和pgBackRest相比,Barman在远程备份和保留策略上做得也很细致,但配置相对更重一些。
选择哪个工具,我个人的经验是:单机构、简单场景,pg_basebackup + crontab + WAL归档完全够用;管理多套PG实例、需要标准化备份恢复流程的,直接上pgBackRest;已经有Barman使用经验的团队,继续用Barman也没问题。工具本身不是决定因素,关键是恢复流程被反复演练过。
5.3 从 XtraBackup 经验看 PG 的备份生态
如果你用过MySQL,大概率对XtraBackup很熟悉。它是一个物理热备工具,通过复制InnoDB数据文件和支持增量备份来工作。换成PostgreSQL生态,很多人会下意识问“PG有没有对应的XtraBackup”,答案是:没有完全对等的工具,但pg_basebackup+WAL归档已经覆盖了XtraBackup的核心能力。
XtraBackup的思路是文件级别的物理备份加redo日志应用,PG的pg_basebackup是数据目录一致性快照加WAL日志应用,本质上都是“基础备份+连续日志”的组合。区别在于XtraBackup可以自己控制增量备份,而PG通常靠WAL归档的连续滚动,配合pgBackRest再做增量管理,所以跨库经验可以平移,但具体命令和恢复模型必须重新学。
我在一线带过不少MySQL转PG的DBA,最常犯的错是拿MySQL的思维方式去套PG,比如习惯性每天做全量物理备份、觉得归档没用。实际上PG的WAL归档能力很强,基础备份没必要天天做,归档连续性和完整性才是核心。
5.4 Windows 与版本选型的小建议
PostgreSQL在Windows下做备份,逻辑和Linux完全一样,只有几个地方容易踩坑。
第一,pg_dump、pg_restore、pg_basebackup这些命令在安装目录的bin子目录下,很多人装了PostgreSQL却找不到命令,是因为没加入PATH环境变量。直接用绝对路径也行,比如C:\Program Files\PostgreSQL\16\bin\pg_dump.exe。
第二,Windows下路径里有空格,命令要加引号,我自己就吃过这个亏:
& "C:\Program Files\PostgreSQL\16\bin\pg_dump.exe" -h localhost -U postgres -Fc -f "D:\backup\mydb.dump" mydb第三,Windows服务启动失败是常见问题。安装完PostgreSQL服务,如果连不上,先看服务管理器和pg_log目录下的日志,很多是数据目录权限不对。这个问题和备份无关,但备份脚本跑不起来往往就是从服务不正常开始的。
版本选型上,总有朋友问“PostgreSQL下载哪个版本”“有没有16便携版”。我的建议是:新项目直接上最新的稳定大版本,比如当前用16或17;生产环境旧版本如果跑得稳,不要为了追新随便升级,升级前先用逻辑备份+pg_upgrade做演练;便携版适合本地测试,不适合生产,因为它通常跳过了一些系统服务和参数默认配置,做备份恢复时行为可能和正常安装版不一致。选版本的重点不是“最新”,而是“你的团队对它足够熟悉,且和驱动、周边工具兼容”,备份方案也一样,关键是稳定可复用,不是花哨。
6. 常踩的坑和备份健康检查清单
分享一些我在实际运维中遇到的真实报错和解决思路,Android和Windows上我都碰到过,整理成速查表方便你遇到问题时快速定位。
6.1 高频报错排查表
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
| pg_dump: error: permission denied for schema | 备份用户缺少schema权限 | 授权USAGE/SELECT,或改用超级用户备份 |
| pg_basebackup: could not receive data from server: ERROR: requested WAL segment ... has already been removed | 主库WAL已被清理,基础备份期间归档不及时 | 加大wal_keep_size,或配置复制槽,确保备份期间WAL完整 |
archive_command返回非0,日志一直报archive_command failed | 归档目录不存在、权限不足或命令语法错误 | 手动执行归档命令排查,确认目录存在且可写 |
恢复时报could not open file,恢复卡住 | restore_command里%p和%f路径不对,找不到归档WAL | 检查restore_command写法,确认归档文件确实在指定目录 |
| pg_restore: error: relation does not exist | 恢复时目标表不存在,或对象依赖顺序不对 | 用--clean --if-exists,必要时先恢复结构再恢复数据 |
| 从库启动后没有进入standby模式 | 没创建standby.signal,或primary_conninfo没配 | 确认数据目录中有standby.signal,检查主从连接配置 |
Windows下执行pg_dump提示不是内部或外部命令 | bin目录未加入PATH | 使用完整路径,或手动添加环境变量 |
6.2 三个独家避坑技巧
第一,备份校验不能少。每次备份完,至少做一次归档可读性校验,比如pg_restore -l能正常列出内容,或对SQL压缩包执行gzip -t。我见过太多备份文件“表面完好”实际已损坏的情况,尤其磁盘老化或异常断电之后。校验成本很低,但能在关键时刻救你一命。
第二,备份恢复前先用pg_controldata检查数据目录状态。它能显示当前数据目录的系统标识符和最近一次检查点位置,如果状态异常,就不要硬着头皮启动。这个工具名称常被忽略,但它在排查物理备份时比任何日志都直接。
第三,备份脚本加上微信/邮件通知。不只是失败要通知,成功也要有记录,因为“失败时没报错”往往比“明确失败”更可怕。我习惯在所有备份任务里做状态标记,失败时进程退出码非0,调度系统盯着退出码即可;同时保留最近一次成功备份的时间戳,如果超过24小时没更新,就要主动排查。
最后说一个我自己的实操习惯:每做一次备份方案升级,我都会在新环境里完整跑一遍从备份到恢复的流程,把恢复时需要的所有命令、路径、用户权限整理成一份文档,放到团队共享。因为备份方案的真正价值不在“备份文件存在”,而在于“灾难发生时,你还能不能按照预案快速站起来”。这套流程我用了很多年,每次救场靠的都是平时扎扎实实的演练,而不是临时翻文档。