2025 年夏天,我在服务器上执行了一条 rm -rf。路径里少了一个空格,删掉的不是临时目录,而是 source 下的一部分文章。命令跑完只花了一秒,终端没有任何提示。
我第一反应是“Git 里有”。打开仓库才想起来,最近两天的修改还没有提交。那两天写了两篇草稿,一篇刚写完,一篇改到一半。它们不在任何备份里。
那次我丢了两天的内容,后来靠记忆重写了一部分,另一部分再也找不回来。损失不算大,但足够让我重新理解“备份”这件事。
把 Git 当备份,是一个常见的误会
Git 很好,但它不是备份系统。
- 它只保存你提交过的内容,未提交的修改不在里面;
- 它默认在同一个磁盘上,磁盘坏了仓库一起坏;
- 它不覆盖数据库、配置、证书和上传的文件;
git push到同一个平台,不等于异地冗余。
我当时的“备份”就是 git push。它保护了历史版本,却没有保护“最近的工作”和“Git 之外的东西”。
现在的结构
我按 3-2-1 的原则重新搭了一遍:至少三份数据,两种介质,一份异地。
- 本机工作副本:日常写作和修改的地方;
- 服务器上的站点目录:构建产物和运行配置;
- 异地备份:另一台机器 + 对象存储。
每天一次增量同步,每周一次全量快照。增量用 rsync,全量用 restic。前者简单直接,后者负责去重、加密和保留策略。
每日同步大概是这样:
1 | rsync -a --delete \ |
--delete 是有意的:镜像要真实反映源目录。但它也意味着源目录被误删时,目标会被同步删除。所以每日镜像之外,必须有带保留策略的全量快照。
每周的全量:
1 | restic backup /srv/axis \ |
restic 的仓库是加密的,密码不放在脚本里,而是通过环境变量或权限受限的密码文件传入。备份里可能有数据库导出和配置文件,不加密等于把钥匙和锁放在一起。
备份成功的标志不是“脚本没报错”
脚本跑完返回 0,只能说明命令执行了,不能说明备份可用。我见过磁盘满的时候 rsync 只同步了一半,也见过对象存储权限变化导致上传静默失败。
所以现在每次备份结束都会做两件事:
- 检查退出码,并把结果写进日志;
- 每周跑一次
restic check,确认仓库结构完整。
更关键的是每季度一次的恢复演练。流程很土:
1 | restic restore latest --target /tmp/restore-test |
随机挑一个文件打开,随机挑一个目录对比,确认内容真的是最近的版本。演练之后把恢复耗时记下来——这个数字比“备份多大”更有意义,因为它决定了故障时我要等多久。
失败也要能被告知
备份最危险的状态不是“没有备份”,而是“以为有,其实早就失败了”。
我给备份脚本加了失败告警:只要退出码非 0、目标端剩余空间低于阈值、或者连续两天没有新的快照,就发通知。通知走的是和站点监控同一条通道,避免再多一个需要维护的系统。
定时任务用 systemd timer,而不是直接写 crontab。好处是能用 systemctl list-timers 看下一次执行时间,日志也能用 journalctl 统一查:
1 | systemctl list-timers --all | grep backup |
备份是一种习惯,不是一次配置
刚配好备份的那几周,我每天都会去看一眼日志。后来它变成习惯,就不再天天看了,但每季度的恢复演练会把我拉回来一次。
现在我对自己只有一条要求:在没有备份的情况下,不做不可逆的操作。 删文件、改数据库、换服务器、升级系统,动手之前先确认最近的备份时间和恢复方式。
这条要求听起来保守,但它让我在几次本来可能很麻烦的操作里,都有一个明确的退路。备份的意义不是“我有很多副本”,而是“我敢做那些必须做的事”。