数据库基础体系 · 第 11/139 篇。文章以各产品官方稳定版本的公开语义为准;示例会明确引擎、事务与部署边界。

数据库备份与恢复:全量、增量、PITR、RPO/RTO 和恢复演练

数据库备份不是“把数据复制一份”这么简单。一个可恢复的备份体系,必须同时回答以下问题:

  • 备份对应哪个一致性时刻?
  • 备份之后发生的修改如何保存?
  • 恢复时如何重放这些修改,并停在目标时刻?
  • 数据库、文件系统、备份工具和对象存储分别保证了什么?
  • 发生误删、实例损坏、存储丢失或区域故障时,最多允许丢多少数据,多久恢复服务?
  • 这些目标是否通过过真实恢复,而不是只通过“备份任务成功”来推断?

本文以 PostgreSQL 和 MySQL 8.4 的公开语义为主要例子。命令中的路径、用户、时间和实例地址需要根据实际部署替换;示例默认事务表使用正常的事务引擎,且不把复制副本直接当作备份。

一、先建立模型:恢复需要“基线”和“变化记录”

设数据库在时间 tt 的一致状态为 S(t)S(t)。数据库运行过程中,事务持续产生变化:

S(t1)=S(t0)Δ(t0,t1)S(t_1)=S(t_0)\oplus \Delta(t_0,t_1)

其中:

  • S(t0)S(t_0) 是某个一致性时刻的基线;
  • Δ(t0,t1)\Delta(t_0,t_1) 是从 t0t_0t1t_1 的所有已提交变化;
  • \oplus 表示按数据库规则应用这些变化。

备份恢复的基本目标,就是取得:

  1. 一个可以独立恢复的基线;
  2. 从基线之后开始、连续且完整的变化记录;
  3. 一个明确的恢复目标。

如果只有基线,没有后续变化,只能恢复到备份时刻。如果只有变化记录,没有可用基线,也无法从空目录中凭日志重建整个数据库。

数据库日志通常不是“每条 SQL 的文本记录”,而是用于恢复的内部变化记录:

  • PostgreSQL 的 WAL(Write-Ahead Log)记录数据页修改等恢复所需信息;
  • MySQL InnoDB 使用 redo log 进行崩溃恢复,binary log 主要记录事务事件,可用于复制和时间点恢复;
  • PostgreSQL 的逻辑复制日志和 MySQL binary log 还可以承担逻辑变化分发,但“可复制”不等于“可以独立恢复整个实例”。

因此,备份系统至少包含以下状态:

一致性全量基线
        +
连续变化日志
        +
日志归档位置、校验信息和元数据
        +
可用的恢复环境

任何一项缺失,都可能导致“备份文件存在,但恢复不出来”。

二、全量备份、增量备份和日志归档

2.1 全量备份

全量备份是一个能够作为恢复起点的完整备份。这里的“全量”有两种常见含义:

  • 物理全量:复制数据库数据目录、表空间文件或存储快照;
  • 逻辑全量:导出表结构、数据、索引定义、权限等逻辑对象。

二者恢复方式不同。

物理全量恢复通常是:

准备空的数据目录
→ 放入物理备份
→ 配置日志恢复
→ 启动数据库
→ 数据库进行崩溃恢复和日志重放

逻辑全量恢复通常是:

创建空实例或目标数据库
→ 执行 DDL
→ 插入数据
→ 创建索引、约束、权限等对象

物理备份通常恢复速度较快,也能保留实例级状态,但通常受版本、平台、目录布局和表空间位置约束。逻辑备份跨版本、跨平台和选择性恢复更灵活,但大数据量下恢复速度可能较慢,并且不能简单等价于整个实例的物理状态。

2.2 增量备份

增量备份只保存相对于某个基线或前次备份发生变化的数据。必须明确它的参照对象:

  • 差异备份:保存自某个全量备份以来的变化;
  • 累积增量:每次都相对于同一个全量;
  • 级联增量:相对于前一次备份。

例如:

周日:全量 F0
周一:相对 F0 的增量 D1
周二:相对 F0 的增量 D2
周三:相对 F0 的增量 D3

恢复周三时,需要:

F0 + D3

如果是级联增量:

周日:全量 F0
周一:I1,基于 F0
周二:I2,基于 I1
周三:I3,基于 I2

恢复周三时,需要:

F0 + I1 + I2 + I3

级联链更节省备份空间,但链中任意一个环节损坏都会影响后续恢复,恢复过程也更复杂。

数据库产品是否原生支持物理增量、增量的格式以及增量链规则,属于产品和版本能力,不能仅凭“有 WAL”或“有 binlog”推断。日志归档本身通常不是传统意义上的“增量备份”:

  • WAL 或 binary log 是连续的变化日志;
  • 增量备份通常是按备份任务生成的某种基线差异;
  • 二者都能减少重复传输,但恢复语义、生命周期和校验方式不同。

2.3 PostgreSQL 的物理备份与 WAL

PostgreSQL 的 pg_basebackup 可以生成物理基础备份。使用 WAL 归档进行恢复前,通常需要配置归档:

# postgresql.conf
archive_mode = on
archive_command = 'test ! -f /backup/wal/%f && cp %p /backup/wal/%f'

修改 archive_mode 通常需要重启;archive_command 返回非零状态时,PostgreSQL 会保留 WAL,直到归档成功。示例中的 cp 只适合演示,生产环境还需要处理并发、目标存储、权限、容量、校验和失败告警。

创建基础备份:

pg_basebackup \
  -h 127.0.0.1 \
  -p 5432 \
  -U backup_user \
  -D /backup/base/2025-03-08 \
  -Fp \
  -X stream \
  -P \
  -R

参数含义:

  • -Fp 使用普通文件格式;
  • -X stream 在备份过程中通过流式方式获取所需 WAL;
  • -P 显示进度;
  • -R 在备份目录中生成连接和恢复相关配置,适合构造备用实例,但恢复到独立时间点时仍应检查和修改配置;
  • -D 必须是空目录或符合工具要求的目标目录。

备份结束不表示远端副本一定可用。对于支持的格式,可以执行:

pg_verifybackup /backup/base/2025-03-08

它主要验证备份目录及其备份清单的一致性,不能替代真正启动数据库并执行业务验证。

2.4 MySQL 的全量、逻辑备份和 binary log

MySQL 8.4 的 mysqldump 可以生成逻辑备份。对主要使用 InnoDB 的数据库,常用:

mysqldump \
  --host=127.0.0.1 \
  --user=backup_user \
  --single-transaction \
  --routines \
  --events \
  --triggers \
  --source-data=2 \
  --all-databases \
  > full.sql

这里:

  • --single-transaction 在事务表场景下通过一致性读减少锁表影响;
  • --routines--events--triggers 用于导出相应对象;
  • --source-data=2 将备份时的 binary log 文件和位置以注释形式写入输出,便于后续定位日志起点;
  • 非事务表、DDL 并发、某些特殊对象和外部系统状态不能简单套用这个一致性假设。

恢复逻辑备份:

mysql --host=127.0.0.1 --user=restore_user < full.sql

这一步恢复的是逻辑对象,不等价于把原实例的所有物理状态原样复制回来。还需要单独核对用户、权限、事件调度器、外部表、加密密钥和应用配置等内容。

MySQL 的 binary log 必须连续保存。恢复到某个时间点时,通常先恢复全量备份,再按正确顺序应用备份之后的 binary log:

mysqlbinlog \
  --start-position=12345 \
  --stop-datetime="2025-03-08 10:30:00" \
  /backup/binlog/binlog.000123 \
  /backup/binlog/binlog.000124 \
  | mysql --host=127.0.0.1 --user=restore_user

实际生产中还要处理:

  • 日志文件之间的连续性;
  • 起始位置是否覆盖全量备份完成之后的第一条需要恢复的事件;
  • 时区和服务器时间;
  • GTID 与文件位置的选择;
  • 多源复制或并行应用场景;
  • 目标实例是否会把恢复产生的事件再次写入 binary log。

mysqlbinlog 的输出是事件序列,不能把任意多个文件不经检查地拼接。恢复前应先用 --start-position--stop-position 或时间过滤查看范围,并在隔离环境中验证。

三、一致性:备份到底代表哪个时刻

3.1 文件复制不等于数据库一致

数据库文件在运行时可能处于以下状态:

  • 一个数据页已经写入新版本;
  • 另一个相关数据页还没有写入;
  • 索引页和表数据页暂时不匹配;
  • 事务只写入了一部分;
  • 内存中的提交状态还没有全部落盘。

因此,直接复制正在运行中的数据目录,可能得到“每个文件都存在,但整体不是一个可恢复状态”的副本。

数据库的物理备份通常依赖两类机制:

  1. 数据库或备份工具建立一致性快照;
  2. 恢复时使用 WAL、redo log 等日志修复备份期间发生的部分写入。

逻辑备份则依赖一致性事务视图、锁或对象级导出规则。mysqldump --single-transaction 的关键条件是事务表和一致性读;如果存在非事务表,或者导出期间执行不兼容的 DDL,就不能继续使用“整个导出天然一致”的假设。

3.2 事务边界与恢复目标

假设某个时间点附近有三个事务:

10:00:00  T1 开始
10:00:03  T1 提交

10:00:04  T2 开始
10:00:08  T2 提交

10:00:07  T3 开始
10:00:12  T3 提交

如果恢复目标为 10:00:10,一个正确的事务型恢复结果不能只看“某条 SQL 是否在 10:00:10 前执行”:

  • T1 已提交,应出现在恢复结果中;
  • T2 已提交,应出现在恢复结果中;
  • T3 尚未提交,不应出现在恢复结果中。

恢复日志通常按数据库内部日志顺序重放,并依据提交状态决定最终可见性。PITR 的目标是恢复到某个数据库一致状态,而不是构造一个任意语句执行到一半的状态。

四、PITR:把数据库恢复到指定时间点

PITR(Point-in-Time Recovery,时间点恢复)是:

恢复一个基础备份
→ 连续应用其后的变化日志
→ 在指定时间、日志位置或事务边界停止

它解决的问题是“恢复到备份之后的某个时刻”,例如:

10:00  基础备份完成
10:20  错误脚本删除订单
10:30  发现问题

如果只恢复 10:00 的全量,10:20 到 10:30 的合法数据也会丢失;如果直接恢复到最新日志,误删也会被重放。PITR 可以尝试恢复到 10:19:59,随后从恢复出的数据库中导出需要的数据。

4.1 PostgreSQL PITR 示例

恢复前准备一个全新的数据目录:

rm -rf /restore/pgdata
mkdir -p /restore/pgdata
cp -a /backup/base/2025-03-08/. /restore/pgdata/
chown -R postgres:postgres /restore/pgdata

然后在恢复目录中配置 WAL 获取方式:

# /restore/pgdata/postgresql.conf
restore_command = 'cp /backup/wal/%f %p'
recovery_target_time = '2025-03-08 10:19:59+00'
recovery_target_action = 'promote'

创建恢复信号文件:

touch /restore/pgdata/recovery.signal
chown postgres:postgres /restore/pgdata/recovery.signal

再以独立实例启动恢复目录:

sudo -u postgres pg_ctl \
  -D /restore/pgdata \
  -o "-p 55432" \
  -l /restore/pg-recovery.log \
  start

恢复过程中,PostgreSQL 会:

  1. 从基础备份加载数据文件;
  2. 根据需要调用 restore_command 获取 WAL;
  3. 重放从基础备份所需位置开始的 WAL;
  4. 到达 recovery_target_time 后停止继续恢复;
  5. recovery_target_action 的设置提升为可读写实例。

需要注意,时间点恢复中的时间必须明确时区。目标时间并不意味着可以恢复到任意一条 SQL 的中间状态;数据库会选择符合自身日志和事务语义的恢复边界。恢复后应检查日志中实际到达的目标位置和时间,而不是只相信命令返回成功。

如果需要保留恢复出的数据,通常先把实例启动在隔离端口,执行校验:

SELECT count(*) FROM orders;
SELECT max(updated_at) FROM orders;

确认后再通过逻辑导出、表级复制或应用切换提取数据。不要直接让一个“可能包含误操作前状态”的恢复实例接管生产流量。

4.2 MySQL PITR 示例

MySQL 常见流程是:

恢复 mysqldump 全量
→ 找到全量对应的 binary log 起点
→ 按顺序应用后续 binary log
→ 在误操作前的时间或位置停止

先查看 binary log 内容:

mysqlbinlog \
  --base64-output=DECODE-ROWS \
  -vv \
  --start-position=12345 \
  --stop-datetime="2025-03-08 10:20:00" \
  /backup/binlog/binlog.000123

确认范围后,再执行恢复:

mysqlbinlog \
  --start-position=12345 \
  --stop-datetime="2025-03-08 10:19:59" \
  /backup/binlog/binlog.000123 \
  /backup/binlog/binlog.000124 \
  | mysql --host=127.0.0.1 --user=restore_user

对于行格式 binary log,-vv 有助于诊断行事件,但显示结果不一定等价于可以直接手工改写的 SQL。恢复时必须使用原始事件流,不能根据展示内容重新编写 SQL 来替代日志重放。

MySQL 的时间停止点同样受时区、事件时间和事务边界影响。对精确恢复要求较高时,使用明确的日志文件和位置,或者基于 GTID 设计恢复流程,通常比只使用墙上时钟更容易审计。

4.3 PITR 的前置条件

PITR 成立至少需要:

可恢复性=可用全量日志连续日志未损坏目标环境可启动目标语义可解释\text{可恢复性} = \text{可用全量} \land \text{日志连续} \land \text{日志未损坏} \land \text{目标环境可启动} \land \text{目标语义可解释}

常见失败包括:

  • 基础备份已删除或校验失败;
  • WAL 或 binary log 在备份完成后没有归档;
  • 日志只保存了部分文件;
  • 备份和日志来自不同实例;
  • 日志文件名称存在但内容截断;
  • 表空间或外部挂载路径缺失;
  • 加密备份没有可用密钥;
  • 恢复目标超出日志保留窗口;
  • 恢复后数据库能启动,但应用依赖的对象、权限或外部配置不完整。

五、RPO 与 RTO:把恢复能力变成可验证的目标

5.1 RPO

RPO(Recovery Point Objective,恢复点目标)表示发生故障时,业务最多能接受丢失多长时间范围内的数据。

如果业务要求 RPO 为 5 分钟,不应简单理解为“每 5 分钟做一次备份”。需要考虑:

RPO实际max(日志产生到远端持久化的延迟,最近可验证日志点与故障点的距离)RPO_{\text{实际}} \approx \max( \text{日志产生到远端持久化的延迟}, \text{最近可验证日志点与故障点的距离} )

例如:

  • 每小时全量备份;
  • WAL 每 10 秒归档;
  • 但归档目标存储故障后,日志积压 20 分钟。

此时实际 RPO 可能接近 20 分钟,而不是 10 秒。

RPO 还要明确“数据”范围。数据库已经提交的事务、消息队列中尚未入库的消息、对象存储文件和外部支付状态,可能不在同一个恢复点上。数据库 PITR 只能恢复数据库本身,不能自动回滚外部系统。

5.2 RTO

RTO(Recovery Time Objective,恢复时间目标)表示故障发生后,业务恢复可用所允许的最长时间。

恢复总时间可以近似拆分为:

RTO=T发现+T决策+T准备环境+T传输或装载备份+T日志重放+T校验+T切换RTO = T_{\text{发现}} + T_{\text{决策}} + T_{\text{准备环境}} + T_{\text{传输或装载备份}} + T_{\text{日志重放}} + T_{\text{校验}} + T_{\text{切换}}

因此,RTO 不仅取决于磁盘吞吐量,还取决于:

  • 是否有预置恢复实例;
  • 备份是否在同一故障域;
  • 全量大小;
  • 日志量和重放速度;
  • DNS、连接池和应用切换耗时;
  • 校验步骤是否被纳入目标;
  • 人工审批和故障沟通时间。

“备份成功”几乎不能直接证明 RTO 达标。只有实际计时恢复,才能知道这些环节的真实耗时。

5.3 RPO、RTO 与复制的关系

同步或异步复制主要服务于可用性和故障切换:

主库提交
→ 复制日志发送
→ 从库接收
→ 从库应用

异步复制存在复制延迟,故障切换时可能丢失尚未到达或尚未应用的事务。同步复制可以降低某类故障下的数据丢失,但仍不能替代备份,因为以下操作可能被同步复制:

  • 错误删除;
  • 错误更新;
  • 恶意修改;
  • 应用漏洞写入坏数据;
  • 逻辑损坏。

主库和从库如果实时复制,坏数据也会实时传播。备份需要具备独立保留周期、独立故障域和必要时的不可变性。

六、恢复演练:验证的不只是文件,而是结果

恢复演练应把“备份系统”当作一个需要验收的系统。至少要验证四个层次:

6.1 介质层

检查:

  • 备份对象是否存在;
  • 文件大小是否异常;
  • 校验和是否匹配;
  • 元数据是否完整;
  • WAL 或 binary log 是否连续;
  • 加密密钥是否能够获取;
  • 备份是否能从生产故障域之外访问。

对象存储上传成功只说明传输完成,不代表备份内容能被数据库恢复。

6.2 数据库启动层

将备份恢复到隔离环境,确认:

  • 数据库进程能启动;
  • 系统目录可读;
  • 表空间路径正确;
  • 日志重放能正常结束;
  • 没有权限、版本或库文件缺失错误。

PostgreSQL 中,如果恢复目录、表空间和 restore_command 配置错误,常见表现是数据库持续等待缺失的 WAL。MySQL 中,如果 binlog 顺序或起点错误,可能出现重复执行、缺少前置事件或 GTID 冲突。

6.3 逻辑与业务层

启动成功不等于数据正确。应验证:

-- 示例:数量、时间范围和关键约束
SELECT count(*) FROM orders;
SELECT min(created_at), max(created_at) FROM orders;
SELECT count(*) FROM orders WHERE customer_id IS NULL;

还应检查:

  • 关键表行数和抽样记录;
  • 主键、唯一约束和外键;
  • 最近一笔已知事务;
  • 被恢复目标明确排除的误操作;
  • 用户、角色和权限;
  • 视图、函数、触发器和定时任务;
  • 应用能否以只读方式执行核心查询。

验证查询必须针对业务语义设计。例如只检查表行数,无法发现“订单数量没变,但金额被错误更新”的问题。

6.4 切换层

最后演练完整切换:

宣布故障
→ 停止或隔离原实例
→ 完成恢复
→ 更新连接入口
→ 清理连接池
→ 执行冒烟测试
→ 观察错误率和延迟
→ 记录恢复完成时间

如果实际生产依赖 DNS、服务发现、代理、连接池或证书,演练也必须经过这些组件。否则得到的只是“数据库实验成功”,而不是“业务恢复成功”。

恢复演练至少应记录:

  • 备份生成时间;
  • 实际可恢复到的最早和最晚时间;
  • 最终恢复点;
  • 全量装载耗时;
  • 日志重放耗时;
  • 业务验证耗时;
  • 从故障到可服务的总耗时;
  • 缺失文件、权限、密钥和人工步骤。

七、常见误解与故障诊断

7.1 “备份任务成功,所以一定能恢复”

错误。备份任务成功可能只表示:

  • 工具读取源目录没有报错;
  • 文件已经上传;
  • 数据库返回了成功状态。

它未必证明远端对象完整、日志连续、密钥可用、恢复目录可启动。应通过校验和、备份清单和定期恢复验证补足证据。

7.2 “从库就是备份”

错误。从库通常会复制最新状态,不能防止逻辑错误传播,也可能共享同一存储、同一地域、同一权限系统。它是高可用或读扩展组件,不是备份的替代品。

7.3 “只保留全量备份就够了”

如果业务允许丢失整个备份周期内的数据,这可能成立;否则不成立。PITR 需要全量之后的连续日志。只有全量而没有日志,恢复点最多停在全量完成时刻。

7.4 “只保留 binlog/WAL,不需要全量”

错误。连续日志依赖可用基线。除非使用了明确支持从某种长期基线重建的产品机制,否则不能从有限日志无限期重建整个数据库。

7.5 “恢复到目标时间后直接开放生产”

风险很高。恢复出的实例可能:

  • 包含目标时间前已经存在的逻辑错误;
  • 缺少目标时间后的合法数据;
  • 用户权限与应用部署不匹配;
  • 仍配置着原主库的复制或归档行为;
  • 触发定时任务、发送通知或重复消费消息。

恢复实例应先隔离网络,关闭不应运行的任务,完成数据和应用验证,再决定是提取部分数据还是整体切换。

八、备份体系的边界与取舍

备份策略应从失败场景反推,而不是只从工具参数出发:

失败场景 主要能力
单个页面损坏 物理备份、日志恢复、校验
实例磁盘损坏 远端全量和连续日志
误删或错误更新 PITR、足够长的日志保留
整机或可用区故障 独立故障域的备份和恢复环境
账号被盗 最小权限、加密、审计、不可变保留
勒索软件删除备份 备份账号隔离、对象锁定或离线副本
版本升级失败 逻辑备份、兼容性测试、回滚方案
外部对象与数据库不一致 跨系统恢复协议、幂等补偿和业务对账

安全方面,备份往往包含比在线数据库更完整、更容易离线复制的敏感数据,因此应:

  • 对传输和静态备份加密;
  • 将加密密钥与备份存储分离管理;
  • 限制备份读取、删除和恢复权限;
  • 审计下载、删除、恢复和密钥使用;
  • 在恢复环境中避免使用生产凭据;
  • 对测试数据执行脱敏,防止恢复副本成为数据泄露源。

最后,备份保留周期也要服从业务风险。保留七天的备份无法应对两周后才发现的数据污染;保留多年但从未演练的备份,也无法证明具备恢复能力。一个完整的恢复目标应写成可执行的形式,例如:

发生单实例故障时:
- RPO 不超过 5 分钟;
- RTO 不超过 30 分钟;
- 恢复到独立实例后,核心订单查询通过;
- 误删发生后,可恢复到误删前的指定事务边界;
- 每月完成一次真实恢复并保留结果记录。

这样的目标才可以被备份链、日志归档、恢复脚本和演练结果逐项验证。


系列导航与关联阅读

官方资料

本文依据数据库官方文档重新梳理;正文、示例与生产检查清单由 WR BLOG 编写。