备份全都在,恢复还是失败了:PITR 实战、恢复演练,以及那些没人告诉你的备份真相

2026-11-02 12 0

凌晨两点,磁盘报警响了。你冲到机房,发现主库的数据卷挂了。运维小张一脸镇定地说:「别慌,备份是好的。」

你松了一口气,让他恢复。小张敲了几行命令,跑了两个小时,屏幕上最后弹出一行:restore failed: archive log 00000001000000000000000042 missing。

那一刻你终于明白了一件早就该明白的事:有备份,和能恢复,是两件事。

这篇不讲怎么敲 pg_dump。我们讲的是更值钱的那部分——当灾难真的来临时,怎么在几小时内而不是几天内,把业务拉起来。


一、你其实有三种"备份",只有一种能救命

先给"备份"这词做个祛魅。市面上能被叫做备份的东西,语义完全不同:

  • 逻辑备份:mysqldump、pg_dump 导出成 SQL 或自定义格式。它导出的是"这一刻的数据长什么样",恢复时要重新执行所有 DDL 和 DML。优点是跨版本、可见可读;缺点是大库恢复以小时计,而且经常恢复到你回不去的某个时间点。
  • 物理全量备份:直接拷贝数据文件或块设备快照。它的本质是"某个瞬间的磁盘快照",恢复快,但要恢复到这个瞬间之后的时刻,你还得靠别的东西。
  • 增量日志归档(WAL / binlog / redo):它是把时间轴补齐的关键。没有它,前两者只能带你回到备份那一刻,而不是任意时刻。

真正能救命的是组合拳:全量备份 + 连续日志归档 + 一个能执行成功的恢复流程。缺任何一环,你的"备份"都只是一个占用存储空间的自我安慰。

一个经典的反直觉事实:如果你的系统每 15 分钟需要归档一次日志,而你的备份恢复目标是"15 分钟内的任意时刻",那么实际上你要求的是零数据丢失,不是"基本不丢"。很多事故复盘里,RPO 这个数字是被想象出来的,不是被演练出来的。


二、PITR:把"任意时刻"变成可能

Point-In-Time Recovery 的原理其实不复杂:先铺一份全量备份,然后在崩溃前把日志一帧不落地归档到异地。恢复的时候,把全量还原出来,再让数据库从日志起点开始重放,直到重放到你指定的那个时间点为止。

关键在最后那句话:"指定的那个时间点"。这一步的精度,取决于日志里记录的到底是什么。

PostgreSQL 的 WAL 里记录的是物理变更(页的字节级修改),所以它天然支持"重放到某个 LSN / 某个时间戳"。MySQL 的 binlog 是逻辑变更(row-based 时是行镜像),同样能按位点或按 GTID 重放。

而这里有一个非常反直觉的坑:

# 灾难发生时,最容易犯的错:把归档一起停了
archive_command = 'pgbackrest --stanza=db archive-push %p'
# 一旦主库挂了、备库还没接上,归档断了,
# 你就永远回不到"崩溃前一秒",只能回落到最后一个成功归档的点

所以真实的架构要求是:归档要独立于数据库进程。PostgreSQL 用 archive_timeout 强制定期切换 WAL,MySQL 用半同步复制或独立 relay 进程推 binlog。理想状态下,数据库进程全挂,外部归档进程还活着,日志还在落盘。

很多团队的做法是"数据库自己推到自己挂载的 NFS 上"。然后 NFS 也挂了、磁盘也满了、备份任务又刚好在同一时间做全量压缩把 IO 打满——这种巧合在真实世界里发生的频率,比你想象的高得多。


三、没验证过的备份,叫"薛定谔的备份"

备份任务每天报绿,容量曲线一路上涨,告警面板上写满了"备份成功"。然后你从没试过恢复过。

这在管理学里叫薛定谔的备份:在你打开它之前,你不知道它是不是一份能用的备份。

常见的失败姿势,按出现频率排序:

1. 恢复环境根本不存在

备份策略写得漂亮,但没人搭过恢复环境。第一次恢复往往是在真正的灾难里,第一次上手,第一次踩坑。这不叫备份,这叫拿生产当测试环境。

2. 校验只做到"文件在"

# 只验证存在性 = 没验证
ls -la /backup/full/
# 真正该做的:跑一次完整性校验 + 试恢复
pgbackrest --stanza=db verify
pgbackrest --stanza=db --type=restore --target=immediate restore
mysqlcheck --check --all-databases

3. 逻辑备份只备份了"数据",忘了"结构之外的东西"

角色、权限、扩展插件、表空间、触发器、函数、序列当前值、事件调度器配置——这些在很多工具的默认行为里是缺失的。等你恢复完发现应用连不上库(因为没有那个角色),或者新建的表没有 RLS 策略(数据全裸奔),就晚了。

4. 恢复依赖了一堆"当时刚好装好的东西"

扩展版本、locale、glibc、pg_config 的编译选项、磁盘挂载点、ulimit、防火墙规则——备份文件本身不携带环境。恢复脚本必须把这些显式重建出来,否则换台机器就废。


四、恢复演练:一个必须反复做的动作

我觉得这是整篇文章里最值钱的一句话:恢复演练的产出不是"能恢复",而是一个数字——恢复时间(RTO)。

演练的目的有四个,每一个都容易被跳过:

  • 量出真实 RTO:从"我决定恢复"到"应用能接流量",中间每一个环节的耗时都要计时。恢复 2TB 的数据、replay 30GB 的 WAL、应用层回放脏数据、缓存预热、灰度放量——这四段时间的总和,才是你真正的 RTO。
  • 发现流程里的隐藏依赖:DNS 记录指向哪台机、密钥从哪拿、配置文件在哪个仓库、"临时提权"找谁。这些东西从不写在文档里,只存在于老员工脑子里。
  • 验证数据是对的:恢复成功后必须跑校验。行数对得上吗?checksum 对得上吗?关键业务表的抽样数据合理吗?一个"恢复成功但数据错乱"的数据库,比一个明确失败的数据库危险得多。
  • 让恢复变成肌肉记忆:事故里最耗时的不是技术,是决策。让值班的人在下半夜能按步骤做,而不是临场发挥。

演练还应该定期"换环境":每季度至少一次在完全独立的机器、不同的存储介质、甚至不同的可用区上做完整恢复。只在原机器原地恢复,验证不了可移植性。


五、逻辑备份的隐藏成本

不能只谈 WAL。对很多中小规模系统来说,逻辑备份反而更实用——跨大版本迁移、从生产拉脱敏数据做测试环境,这些场景逻辑备份是唯一解。

但它有几个必须知道的成本:

恢复时间与数据量成正比,且远慢于物理恢复。恢复 500GB 的逻辑备份,SQL 逐条执行可能要好几个小时,而且中间某一条报错,你不知道前面执行到哪了。

# 恢复时的常见灾难现场
ERROR: duplicate key value violates unique constraint "users_pkey"
# 因为 dump 是"先删后插"还是"纯插入",语义完全不同
# 用 --clean --if-exists 之前,先确认你的事务边界

大表锁。纯文本格式的 dump 在恢复时会长时间持有锁,某些配置下还会触发 autovacuum 风暴。更稳妥的做法是用并行 dump + 自定义格式并行恢复:

# 并行导出(目录格式,支持 -j 并行恢复)
pg_dump -Fd -j 8 -f /backup/pgdata
# 并行恢复
pg_restore -j 8 -d targetdb /backup/pgdata

# MySQL 侧:不要用 --single-transaction 之外的花活去 dump InnoDB
mysqldump --single-transaction --quick --set-gtid-purged=OFF \
  --routines --triggers --events --databases prod

注意 --set-gtid-purged=OFF 这种细节:恢复出来的实例如果带着旧 GTID 集合,复制会直接罢工。这类"恢复成功但复制起不来"的问题,文档里很少写,只能自己踩。


六、一份能落地的备份检查清单

  1. 三层备份结构:日级全量 + 连续日志归档 + 归档本身的异地副本。日志归档的可用性优先级甚至高于全量备份。
  2. 备份与生产隔离:不同的凭据、不同的账号、不同的网络域。备份账号应该是只读的,不能删备份。
  3. 加密与生命周期:备份文件里有全量明文用户数据,权限和加密等级必须与生产同级;过期策略要自动执行,且"自动删除备份"的权限要跟"创建备份"的权限分开,否则一个手滑的脚本能把你最后的退路一起删掉。
  4. 自动化校验:每天校验完整性,每周做一次随机时间点的恢复演练,每月在另一个可用区做一次全流程演练。演练结果要有报表,RTO 变化超过阈值要报警。
  5. 文档里只写"步骤",不写"知识":把所有隐式依赖显式化。恢复手册应该是任何值班的人都能照着做的东西,而不是只有某位老哥知道。
  6. 演练一次"备份被删":检查有没有第二份不可变副本(对象存储 WORM、跨账号复制、离线磁带)。勒索软件加密备份目录这种剧本,早就不新鲜了。

七、最后

备份这件事有个反直觉的地方:它的产出不在你备份成功的那一刻,而在你恢复成功的那一刻。

每天凌晨三点自动跑的任务全部成功,那个绿灯什么都不证明。它只证明"脚本没报错"。

真正能让你在半夜两点不心跳加速的,是那份写满具体步骤的恢复手册、那份每个月真的被恢复过的验证记录、和那个知道 RTO 具体是多少秒的人。

至于那些躺在存储里、从没被读过的备份文件——它们不是保险,是装饰品。

(顺便,如果你正在读这段文字,说明你的备份至少还有机会被验证一次。趁现在还来得及。)

相关文章

慢 SQL 优化实战:从 8 秒到 30 毫秒,我修的不是代码,是索引
你的监控把你自己监控崩了:Prometheus 标签基数爆炸,一个 user_id 标签吃掉 8G 内存
你的服务不慢,是卡住了:eBPF 火焰图实战,5 分钟定位线上 P99 飙升
你的服务每秒在做几千次加密握手,而 99% 的握手都是白干的:TLS 连接复用的真相
你的接口重试三次,数据库就多扣三次钱:幂等性设计的正确姿势
数据库慢得像个拖拉机?先别骂数据库,这 7 个坑有一半是你自己挖的

发布评论