事情是这样的,上周五晚上我正躺在床上刷手机,突然收到一条钉钉报警——备份任务失败。我那个备份脚本跑了好几年都没出过问题,当时心里就咯噔一下。
我赶紧爬起来打开电脑,ssh到备份服务器上看了看日志。报错信息是`rsync error: some files could not be transferred (code 23)`,后面跟着一堆`Permission denied`。扫了一眼,是某个开发同事把生产环境的一个目录权限改成了700,而我的备份用户不是目录所有者,自然读不了。
这个备份脚本是我三年前写的,用的是最简单的rsync + crontab方案。每天凌晨2点,rsync把整个/data目录同步到一台备份机器上,保留7天的增量备份。当时觉得这方案够用了——小公司嘛,数据量不大,出问题大不了从备份机拉回来。
但这次报警让我重新审视了一件事:我的备份策略太脆弱了。
先说几个具体的问题。第一,rsync的增量备份依赖于硬链接,但一旦源端文件权限变化,rsync直接报错退出,整轮备份就停了。第二,我没有任何备份验证机制——备份跑完了,到底能不能恢复,我从来没测过。第三,备份机跟生产机在同一个机房,真要是机房着火或者交换机挂了,那就是一锅端。
这些隐患其实我早就知道,但一直懒得改。直到这次报警,加上前阵子读到某云厂商用户数据丢失的案例,我决定花一个周末把备份策略重新搞一遍。
我的目标很简单:备份不能因为单个文件权限问题就全盘失败;备份要能做恢复演练;另外要搞一个异地备份,至少跨机房。
第一步,我把rsync替换成了restic。restic是一个用Go写的备份工具,支持增量备份、加密、快照管理,而且不依赖文件权限去读数据——它直接读磁盘块。版本我用的是restic 0.16.4,从GitHub release页下的二进制,直接扔到/usr/local/bin。
安装很简单:
“`
wget https://github.com/restic/restic/releases/download/v0.16.4/restic_0.16.4_linux_amd64.bz2
bunzip2 restic_0.16.4_linux_amd64.bz2
chmod +x restic_0.16.4_linux_amd64
mv restic_0.16.4_linux_amd64 /usr/local/bin/restic
“`
初始化仓库:
“`
restic init –repo /backup/restic-repo
“`
这里要输入一个密码,我用了openssl随机生成了一个256位的key,存到了/etc/backup/pass.txt,权限设成600。
然后写了一个备份脚本,核心逻辑是:
“`
#!/bin/bash
export RESTIC_PASSWORD_FILE=/etc/backup/pass.txt
restic backup /data –repo /backup/restic-repo \
–exclude=”*.tmp” \
–exclude=”*.log” \
–exclude=”/data/tmp” \
–tag daily-$(date +%Y%m%d) \
2>> /var/log/backup.err.log
“`
加了几个排除规则——临时文件和日志文件没必要备份,节省空间和带宽。restic的增量备份是自动的,它只会上传改变的数据块,第一次全量大概花了40分钟(数据量大概500GB),后续增量都在几秒到几分钟。
但光有本地备份不够。我又在另一个机房租了一台4核8G的轻量服务器,2T硬盘,每个月两百多块钱。在这台远程机器上也装了restic,然后通过rsync把本地备份仓库同步过去。
等等,我是不是又用回rsync了?不是的,这里有个关键区别——我同步的是restic的备份仓库,而不是原始数据。restic仓库里是一堆加密后的数据块,即使传输过程中被人截获,他也读不出任何内容。而且因为仓库本身是增量结构,rsync同步的时候只会传变化的数据块,效率很高。
远程同步的脚本写了个简单的:
“`
rsync -avz –delete /backup/restic-repo/ backupuser@remote-server:/backup/restic-repo/
“`
crontab里每天凌晨3点跑本地备份,4点跑远程同步。错开时间,避免IO争抢。
但到这里还没完。备份最怕的是:你以为有备份,真到恢复的时候发现恢复不了。所以我又加了一个恢复验证的环节。
每天备份完成后,脚本会随机选一个快照,在/backup/test-restore目录下执行一次恢复到临时目录的操作:
“`
SNAPSHOT_ID=$(restic snapshots –repo /backup/restic-repo –latest 2 –json | jq -r ‘.[0].short_id’)
restic restore $SNAPSHOT_ID –repo /backup/restic-repo –target /backup/test-restore –include /data/mysql/dump
“`
然后检查恢复出来的文件是否存在、大小是否大于0。如果连续3次验证失败,就发邮件报警。这一步虽然简单,但能让我睡觉踏实一点。
另外说一个踩坑的地方:restic默认的备份策略是保留最近7天的快照,但我发现如果不设置清理策略,仓库会无限膨胀。所以加了一个forget命令:
“`
restic forget –repo /backup/restic-repo \
–keep-daily 7 \
–keep-weekly 4 \
–keep-monthly 6 \
–prune
“`
这个配置的意思是:保留最近7天的每日快照、最近4周的每周快照、最近6个月的每月快照,其余的删掉。`–prune`参数会真正释放磁盘空间,否则只是标记删除。
第一次跑`–prune`的时候我踩了个坑——清理过程极其慢,跑了快两个小时,CPU和磁盘IO都被打满了。后来查文档才知道,prune操作需要重新打包所有数据块,数据量越大越慢。所以我把prune从每天的脚本里移出来,改成了每周日跑一次。
整个调整完成后,我手动模拟了几种故障场景:
– 删掉某个关键目录,用restic restore恢复,成功。
– 把备份机硬盘拔了,从远程仓库恢复,成功,但速度比较慢——1M带宽,500GB的数据恢复理论要5天。这个没办法,只能忍,真出大事就找辆货车把硬盘拉过去。
最后说几个经验总结。
不要相信任何一个没有验证过的备份。我见过太多人搭好备份就忘了,等到真需要恢复的时候才发现备份文件是坏的、配置是错的。我现在的做法是每个月手动做一次完整的恢复演练,从零开始搭一台临时服务器,把最近的一个快照恢复到这台机器上,跑一下应用确认能用。
还有,备份策略不是在服务器上写完脚本就完事了。文档要写清楚:备份存哪、密码在哪、恢复步骤是什么。万一你不在公司,别人要恢复数据怎么办?我把这些信息写进了一个README.md,放在/backup目录下,权限644,所有人都能读。
另外,监控不能少。我写了个简单的健康检查脚本,每天检查restic仓库的完整性:
“`
restic check –repo /backup/restic-repo 2>&1 | grep -q “no errors were found” || echo “Check failed” | mail -s “Backup check failed” my@email.com
“`
这个check命令会验证所有数据块的哈希值,确保仓库没损坏。
现在这套策略跑了一个多月,每天备份时间从原来的40分钟降到了3-5分钟,磁盘占用量反而比原来少了30%(因为不用保留7份全量硬链接了)。远程同步每天大概传200-500MB的数据,基本不影响业务带宽。
唯一不太爽的是,restic的restore速度比rsync慢——rsync直接拉文件,restic需要解密和解压。但在可接受范围内,毕竟加密换来的是安全。
备份这件事,说白了就是个成本问题:你需要多少安全,就愿意付出多少成本。我这套方案,每月多花两百块服务器钱加一个周末的时间,换来的是一旦出事能救回数据。值不值,看个人。反正我是再也不想半夜爬起来修备份了。