我一直觉得备份是件很枯燥的事。
直到有一次,我在服务器上执行了一条 DELETE FROM comments WHERE id > 100,本来想删测试数据,结果忘了自己不在测试库。
那条语句跑完,生产库少了两千多条评论。
我坐在那儿愣了大概十秒钟,然后想起来,昨天晚上的备份还在。
从那次之后,我对备份的态度完全变了。
备份到底要解决什么问题
先说清楚,备份不是"把数据复制一份"这么简单。
它要能应对几种不同的场景。
误删数据,需要恢复到删除前的某个时间点。
磁盘损坏,需要换台机器把服务重新拉起来。
程序 bug 写坏了数据,需要回滚到某个版本。
被勒索软件加密,需要有离线副本。
这几种场景对备份的要求不一样。误删需要时间点密集,磁盘损坏需要副本在别的机器上,勒索需要副本不可写。
所以一个靠谱的方案,至少要有三个特征。定期自动跑,副本存在别的地方,而且真的能恢复。
最后一条最容易被忽略。很多人备份跑了半年,真要恢复的时候才发现文件是空的。
最基础的 mysqldump
小站用这个就够了。
|
|
几个参数都值得说一下。
--single-transaction 是关键。它在 InnoDB 上开一个一致性快照,导出过程中不会锁表,业务可以继续写。导出来的数据是开始那一刻的状态,不会出现前后不一致。
注意这个参数只对 InnoDB 有效。如果你的表还有 MyISAM,那就得用 --lock-tables,会锁表,业务会卡。
--quick 让 mysqldump 一行一行往客户端发,而不是全部读进内存。大表不加这个,很容易把内存撑爆。
--routines --triggers --events 这三个,默认是不导出的。不加上,恢复之后存储过程、触发器、定时任务全都没了。我第一次恢复的时候就踩了这个坑,恢复完发现定时清理任务不跑了。
--default-character-set=utf8mb4 防止中文乱码。如果源库是 utf8mb4,导出的时候不指定,有时候会被转成 latin1。
加压缩和加密
纯 sql 文件很大,压缩一下能小到十分之一:
|
|
如果数据里有敏感信息,还得加密:
|
|
-pbkdf2 是让 openssl 用更安全的密钥派生方式,不加的话会有警告。
-pass file: 从文件读密码,比写在命令行里安全,因为命令行参数在 ps 里能看到。
/etc/backup.key 权限设成 600,只有 root 能读。
恢复的时候:
|
|
写成脚本
手动跑总会忘,得自动化。
|
|
set -euo pipefail 这三件套一定要加。-e 让任何命令失败就退出,-u 让未定义变量报错,-o pipefail 让管道里任何一个环节失败都能被捕获。
没有 pipefail 的时候,mysqldump | gzip 如果 mysqldump 失败了,整个管道还是返回成功,因为 gzip 成功了。这是很隐蔽的坑,我吃过一次,备份文件是空的,脚本还报成功。
那个 SIZE 校验也是必须的。空文件、截断的文件,靠退出码是发现不了的。
用 cron 跑
|
|
每天凌晨三点半跑。这个时间业务量最低。
cron.d 的格式比 crontab -e 多一个用户字段,就是 root 那个位置。写错了会不执行,而且没有任何提示。
增量备份
全量备份对数据量小的站够用,但如果库有几个 G,每天全量就不太现实了。
增量有两种做法。
一种是 binlog。MySQL 的 binlog 记录了所有数据变更,配上定期全量,可以把数据恢复到任意时间点。
先确认 binlog 开着:
|
|
如果是 ON,那 binlog 文件在 datadir 下面,名字形如 mysql-bin.000001。
备份 binlog:
|
|
这个会持续拉取新的 binlog。实际用的时候一般配合 --result-file 和定期切换。
另一种是 --incremental 配合 xtrabackup。Percona 的工具,支持热备和增量,适合大库。不过它需要单独装,配置也复杂一些。
我这个小站没到那个量级,用的还是每天全量。库里不到一百兆,压缩完十几兆,存三十天也就四百多兆。
存到别的地方
本地备份只能防误删,防不了磁盘坏或者机器没了。
所以至少要有两份,一份在本机,一份在别处。
我用的是 rclone 同步到对象存储:
|
|
--max-age 24h 只同步一天内的新文件,不用每次全传。
对象存储那边开了版本控制,就算本地文件被勒索软件加密了,云上的历史版本还在。
还有一个细节,云上的凭据要单独放,不要和备份放在同一台机器上。不然机器被攻破,凭据一起泄露,备份也保不住。
真正的考验是恢复
这一步我建议每个人都做一次。
找个测试环境,把备份文件恢复进去,看看数据对不对。
|
|
然后:
|
|
数字对得上,时间对得上,才算备份有效。
我第一次做这个演练的时候,发现恢复出来的库少了三张表。原因是那三张表是 MyISAM,--single-transaction 对它们无效,mysqldump 直接跳过了。后来改成全部 InnoDB 才好。
这种问题,只有真的恢复一次才能发现。
恢复的流程也要写下来
出事的时候人是慌的,容易漏步骤。
我把恢复流程写成了一个文档,放在 /srv/docs/restore.md,内容包括:
先停掉应用,防止有新的写入。
备份当前的数据库(哪怕是坏的,先留一份)。
创建新的库,导入备份。
检查表数量、行数、最新的记录时间。
确认无误之后,改应用的连接配置,启动。
最后记录这次事故的时间线和原因。
有这个文档,真出事的时候照着做就行,不用临场想。
现在的情况
这套东西跑了快一年,中间用上了两次。一次是我自己误删,一次是磁盘满了导致写入失败。
两次都在半小时内恢复,业务中断时间不到一小时。
备份这件事,平时感觉不到它的价值。它唯一的价值就在出事的那一刻,而那一刻你没有任何补救的机会,只能靠之前准备好的东西。
所以我现在对它的态度是,不追求多先进,但一定要简单、可靠、验证过。