"我们有备份"这句话在真正需要恢复的那一刻才有含义。判断一个备份体系是否成立,不看备份任务的成功率,而看最近一次从备份恢复出一个可用实例是什么时候、花了多久、丢了多少数据。这篇按策略、方式、校验、演练的顺序拆。
1. 三要素:RPO、RTO 与保留期
| 要素 | 回答的问题 | 决定的技术手段 | 成本方向 |
|---|---|---|---|
| RPO | 最多能丢多少数据 | 备份频率 + binlog 持续归档 | 越小越贵 |
| RTO | 多久必须恢复可用 | 备份形态(物理/逻辑)、是否预热备用实例 | 越小越贵 |
| 保留期 | 要留多久 | 存储介质与归档层 | 越长越贵 |
三者互相拉扯:RPO 做到 1 分钟,意味着必须有持续增量归档;RTO 做到 10 分钟,意味着增量恢复链条要短、或者干脆有热备。把三个数字先写下来,再选工具——反过来"先选工具再定指标"通常得到一套跑得动但恢复不了的流程。
一个可以照抄的表述方式:「RPO 5 分钟,RTO 30 分钟,全量保留 7 天、归档保留 90 天」。指标不写清楚,演练时也无法判断通过与否。
2. 备份类型与恢复链条
| 类型 | 每次的输入 | 恢复需要 | 空间与时间 |
|---|---|---|---|
| 全量 | 全部数据 | 单个文件即可 | 空间最大,恢复最简单 |
| 增量 | 上次备份以来的变化 | 全量 + 全部中间增量 | 空间最小,链条最长 |
| 差异 | 上次全量以来的变化 | 全量 + 最近一次差异 | 折中 |
恢复链条的长度直接等于风险:增量策略里只要中间有一份损坏或缺失,链条就断在那里,后面的增量全部用不了。所以增量备份必须配"定期全量 + 链条完整性校验",并且要在备份清单里写清每一份增量的父节点是谁。
3. MySQL:三种方式对照
| 方式 | 一致性保证 | 恢复粒度 | 适用 |
|---|---|---|---|
mysqldump |
需 --single-transaction(InnoDB) |
库/表 | 中小库、跨版本迁移 |
xtrabackup |
物理一致,支持增量 | 实例 | 大库、RTO 要求低 |
| binlog 归档 | 逐事务 | 时间点 | 配合全量做 PITR |
逻辑备份的一致性关键在 --single-transaction:它在可重复读隔离级别下开一个事务拿到一致快照,避免备份期间写入导致的不一致。但这条命令只对 InnoDB 有效,MyISAM 表仍会锁表或产生不一致。
$ mysqldump --single-transaction --quick --routines --triggers --events \
--set-gtid-purged=OFF --default-character-set=utf8mb4 \
-u backup -p dbname > /backup/db_$(date +%F_%H%M).sql
--routines、--triggers、--events 三个必须显式加,否则存储过程、触发器、定时事件全部丢失——这类丢失在恢复后往往几周才被发现。
物理备份:
$ xtrabackup --backup --target-dir=/backup/xtra/$(date +%F) \
--user=backup --password=***
$ xtrabackup --prepare --target-dir=/backup/xtra/$(date +%F)
--prepare 是必须的一步,它回放未提交事务、让数据文件达到一致状态;只 --backup 不 --prepare 的目录不能直接启动。
PITR 的完整流程是四步:先恢复最近一次全量,再重放从全量结束到目标时刻之间的 binlog,最后 RESET MASTER/PURGE 并重新挂接复制。缺一步都会得到一个"看起来能启动但数据停在更早时刻"的实例。
$ mysqlbinlog --stop-datetime='2026-11-09 03:00:00' \
/backup/binlog/bin.000123 /backup/binlog/bin.000124 | mysql -u root -p
--stop-datetime 与 --start-datetime 的时区基准是 binlog 里记录的时间,而 binlog 时间取的是服务器时区。备份文件的时间戳、binlog 的时间戳、你笔记本上的时间,三者不一致是 PITR 失败的常见原因。
4. 文件备份:不能热拷
数据库文件在运行中被修改,直接 cp 得到的是一堆互相不一致的页。可行的手段只有三种:停服务拷、文件系统快照、或让数据库自己产出备份。
快照的原子性取决于卷管理器:LVM 快照、云盘快照通常是瞬时一致点;跨多个卷的"逐个打快照"则不是原子操作,中间有时间差。要让多个卷一致,得先 fsfreeze 再打快照再解冻。
$ fsfreeze -f /data && lvcreate -L 20G -s -n snap_data /dev/vg0/data && fsfreeze -u /data
fsfreeze 期间写入会被挂起,时间窗口必须短;忘了 -u 会让挂载点一直冻结,表现为所有写操作卡住——这类"备份把业务弄挂"的事故并不少见。
5. 存放:异地、介质、加密
| 维度 | 最低要求 | 常见失误 |
|---|---|---|
| 位置 | 与源不同机器、不同可用区 | 备份和数据库在同一块盘上 |
| 介质 | 与生产盘不同物理设备 | 同一块云盘挂载成两个目录 |
| 加密 | 传输 TLS + 静态加密 | 明文 tar 传到对象存储公共桶 |
| 权限 | 备份账号只读 + 目录 700 | 备份目录 world-readable |
| 版本 | 每份带版本与校验和 | 只有文件名,无法判断内容 |
"备份在同一块盘上"是最常见的假备份:盘坏了两样一起没。异地这一条不是合规要求,是故障域的常识。
6. 校验的四层
| 层 | 做什么 | 能发现什么 | 频率 |
|---|---|---|---|
| 1 完整性 | 文件大小、校验和、gzip -t |
传输截断、磁盘坏块 | 每次备份后 |
| 2 可恢复性 | 恢复到临时实例并启动 | 权限、版本、参数不匹配 | 每周 |
| 3 内容抽样 | 行数、关键表、最大 ID 对比 | 逻辑丢失、字符集损坏 | 每次恢复演练 |
| 4 演练记录 | 记录耗时、步骤、偏差 | 流程本身的缺陷 | 每月 |
前两层自动化,第三层半自动,第四层必须人做。只做第一层等于只验了文件没坏,而"文件没坏但恢复不起来"是更常见的失败模式。
$ sha256sum /backup/db_*.sql > /backup/SHA256SUMS
$ gzip -t /backup/db_2026-11-09.sql.gz && echo ok
$ mysql -e 'SELECT COUNT(*) FROM db.orders' > /tmp/after.txt
$ diff <(sort /tmp/before.txt) <(sort /tmp/after.txt)
7. 恢复演练:步骤清单
# 1) 隔离环境:不同端口、不同目录、不同实例名
$ mysqld --initialize-insecure --datadir=/restore/data
$ mysqld --datadir=/restore/data --port=3307 --socket=/restore/mysql.sock &
# 2) 导入
$ mysql --socket=/restore/mysql.sock -u root < /backup/db_2026-11-09.sql
# 3) 对齐 binlog 到目标时刻
$ mysqlbinlog --stop-datetime='...' bin.000123 | mysql --socket=/restore/mysql.sock -u root
# 4) 抽样比对
$ mysql --socket=/restore/mysql.sock -e 'CHECKSUM TABLE db.orders'
清单里必须固定三样记录:开始时间、可查询时间、业务可用时间。第一个数字说明备份本身多大,第二个说明恢复脚本有效,第三个才是 RTO 的真实值——通常比第二个大得多,因为还要切流量、改配置、验证接口。
演练要定期换人执行。写脚本的人做演练会无意识地跳过"他不知道但别人会踩"的步骤。
8. 恢复后的收尾
| 项 | 检查 | 不处理的后果 |
|---|---|---|
| 账号与权限 | 应用账号是否存在、密码是否正确 | 应用连不上,或权限过大 |
AUTO_INCREMENT |
各表当前值 | 新写入撞主键,报重复键 |
| 字符集与排序 | 库/表/连接三层一致 | 中文乱码、排序错乱 |
| 复制 | CHANGE REPLICATION SOURCE TO 重建 |
从库停在旧位点 |
| 定时事件 | event_scheduler 是否开启 |
定时任务静默不跑 |
| 外键与约束 | SET FOREIGN_KEY_CHECKS 是否被遗留为 0 |
数据完整性失效 |
AUTO_INCREMENT 是恢复后最容易漏的一项:逻辑备份不带自增计数器时,恢复后计数器从最大值重新算,如果有旧数据被删过,新插入就可能撞已存在的 ID。
9. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 备份任务一直成功,恢复时导入失败 | 版本不一致或 SQL 模式差异 | 记录源/目标版本,恢复前 SET sql_mode 对齐 |
| 恢复后发现存储过程全没了 | mysqldump 未带 --routines |
重备一次,并写进脚本模板 |
| PITR 后数据停在更早时刻 | binlog 时间基准与预期时区不一致 | 统一时区,用 --start-position 精确控制 |
| 增量恢复链断在中途 | 某份增量缺失或损坏 | 改为"全量 + 差异",并校验链条父节点 |
| 恢复把生产库覆盖了 | 演练与生产用了同一个实例/端口 | 强制隔离端口与 socket,加 --one-database 白名单 |
xtrabackup 恢复后启动失败 |
漏了 --prepare |
补 prepare 再启动 |
| 备份盘写满导致备份截断 | 未做容量告警与轮转 | 加容量阈值告警,保留策略落地 |
| 备份文件读不出 | 加密密钥丢失或权限错误 | 密钥与备份分开保管但可追溯 |
10. 边界
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 只防误删单表 | 延迟复制从库 / 表级闪回 | 全量恢复代价过大 |
| 跨机房的强一致 | 同步复制或共识协议 | 备份不等于高可用 |
| 需要秒级 RPO | binlog 实时投递 + 对象存储 | 定时任务做不到 |
| 归档合规 | 对象存储归档层 + 保留锁 | 本地磁盘无法证明不可篡改 |
| 巨型库 | 物理备份 + 增量 + 并行流 | 逻辑导出时间不可接受 |
11. 自测
- RPO 与 RTO 分别约束哪一环?为什么缩短 RTO 往往要先缩短恢复链条?
mysqldump --single-transaction对 MyISAM 表为什么无效?xtrabackup只做--backup不做--prepare,恢复后会怎样?- PITR 时数据停在比预期更早的时刻,最先核对什么基准?
- 为什么说"备份和生产在同一块盘上"等于没有备份?
- 恢复后新插入数据撞主键,通常是哪一项没处理?