一、用 RPO 和 RTO 定义恢复能力
RPO 是最多允许丢失的数据时间,RTO 是从事故发生到业务恢复的时间。每天全量备份无法满足 5 分钟 RPO,必须持续归档 binlog;30 分钟 RTO 则要求预建恢复流程并定期测量真实耗时。
保留期要大于发现故障最长时间、恢复演练耗时和安全缓冲。备份文件存在不代表可用,只有成功恢复并通过业务校验才算合格。
Primary MySQL
├─ 一致性全量备份 → 加密、不可变、跨区域存储
└─ Binlog 连续归档 → 文件/GTID/时间索引
↓
事故 → 隔离恢复 → 全量基线 → 回放到目标点 → 校验 → 切流二、启用并核对 Binlog
使用 ROW 格式记录确定的数据变化,sync_binlog 与 innodb_flush_log_at_trx_commit 提高提交持久性但有性能代价。变更前验证参数是否动态生效和重启窗口。
[mysqld]
server_id=101
log_bin=mysql-bin
binlog_format=ROW
binlog_row_image=FULL
binlog_expire_logs_seconds=1209600
sync_binlog=1
innodb_flush_log_at_trx_commit=1SHOW VARIABLES WHERE Variable_name IN ('log_bin','binlog_format','binlog_row_image','binlog_expire_logs_seconds','sync_binlog','innodb_flush_log_at_trx_commit');
SHOW MASTER STATUS;
SELECT @@server_uuid, @@version, @@global.gtid_executed;三、创建一致性全量备份
single-transaction 适合事务表一致性快照,期间避免 DDL。超大实例评估 MySQL Shell dump 或物理备份,并在隔离环境测吞吐。校验和只能证明文件未变,不能证明能够恢复。
BACKUP_DIR=/backup/mysql/full-$(date +%F-%H%M%S)
mkdir -p "$BACKUP_DIR"
mysqldump --single-transaction --routines --events --triggers \
--set-gtid-purged=OFF --source-data=2 --hex-blob \
--default-character-set=utf8mb4 --databases app_db \
| gzip -1 > "$BACKUP_DIR/app_db.sql.gz"
sha256sum "$BACKUP_DIR/app_db.sql.gz" > "$BACKUP_DIR/SHA256SUMS"
mysql -NBe 'SELECT @@version,@@server_uuid,@@global.gtid_executed' > "$BACKUP_DIR/metadata.txt"四、持续归档 Binlog
归档账户只授予复制日志所需权限,密码由秘密文件提供,不写命令历史。监控最后归档时间、文件连续性、磁盘和异地上传;远端确认前不删除本地文件。
mkdir -p /backup/mysql/binlog
mysqlbinlog --read-from-remote-server --raw --stop-never \
--host=mysql-primary --user=binlog_archiver \
--result-file=/backup/mysql/binlog/ mysql-bin.000001
find /backup/mysql/binlog -type f -name 'mysql-bin.*' -print0 | sort -z | xargs -0 sha256sum五、构造误删演练并记录时间线
演练使用隔离 schema。误删前后同时记录数据库时间、请求 ID 和 binlog 坐标;生产目标点不能只靠人工记忆的时钟,必须从审计、应用日志和 event 边界交叉确认。
CREATE TABLE recovery_lab.orders (id BIGINT PRIMARY KEY,status VARCHAR(32) NOT NULL,amount DECIMAL(12,2) NOT NULL,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB;
INSERT INTO recovery_lab.orders VALUES (1001,'paid',199.00,NOW()),(1002,'pending',88.00,NOW());
SELECT NOW(6) AS before_accident_time;
DELETE FROM recovery_lab.orders WHERE id=1001;
SELECT NOW(6) AS after_accident_time;六、在隔离实例恢复全量基线
恢复实例不能连接生产消费者、定时任务、邮件或支付系统,避免历史数据触发真实副作用。使用独立网络和凭据,完成校验前不覆盖生产。
sha256sum -c /backup/mysql/full-*/SHA256SUMS
zcat /backup/mysql/full-*/app_db.sql.gz | mysql --host=recovery-mysql --user=root -p
mysql --host=recovery-mysql --user=root -p -e 'SELECT COUNT(*) FROM recovery_lab.orders; SHOW WARNINGS;'七、定位事务边界并回放到目标点
时间边界可能落在事务内部。先用 mysqlbinlog 解码查看事务开始和提交,正式回放优先按 position 或 GTID 形成完整边界,绝不把半个事务送入恢复库。
mysqlbinlog --base64-output=DECODE-ROWS -vv \
--start-datetime='2026-09-26 09:00:00' --stop-datetime='2026-09-26 09:30:00' \
/backup/mysql/binlog/mysql-bin.000123 | less
mysqlbinlog --start-position=154 --stop-position=9821 \
/backup/mysql/binlog/mysql-bin.000123 \
| mysql --host=recovery-mysql --user=root -p八、结构、业务和应用三层校验
进程退出码为零不等于恢复正确。先检查表、约束和字符集,再验证行数、金额、库存等业务不变量,最后让应用以只读方式执行关键查询。大表按主键区间对账,抽样只能补充。
SELECT COUNT(*) AS row_count,SUM(amount) AS total_amount FROM recovery_lab.orders;
SELECT * FROM recovery_lab.orders WHERE id IN (1001,1002);
CHECK TABLE recovery_lab.orders;
SELECT TABLE_NAME,TABLE_ROWS FROM information_schema.tables WHERE TABLE_SCHEMA='recovery_lab';九、切流、回退与重新保护
冻结旧主库写入并记录最终坐标。恢复库通过三层校验后,从少量只读流量开始;旧库保持只读快照和明确观察窗口。切换完成立即为新主库建立新的全量基线与 binlog 归档。
- 切换前记录 DNS、连接池和应用配置回滚值。
- 失败条件量化为错误率、对账差异和延迟阈值。
- 旧库不立即删除,过观察窗后按审批清理。
十、自动演练与审计
至少每月自动恢复最近备份,记录实际 RPO、RTO、吞吐和失败步骤。备份加密密钥与数据分开保存,并验证密钥轮换后历史备份仍能恢复。告警要覆盖归档断档而非只看任务退出码。
- 随机选择恢复时间点,避免脚本只适配固定样例。
- 恢复后运行结构和业务断言。
- 报告保存备份哈希、坐标、版本与责任人。
- 未达到目标时创建有截止时间的改进项。
总结
可用备份的唯一证明是成功恢复并通过业务校验。以全量备份建立一致基线,以连续 binlog 缩小 RPO,在隔离环境按完整事务边界回放,再通过结构、业务和应用三层校验切流,才能把文件变成恢复能力。