Guide · 通用

没恢复过的备份不算备份:一个人管线上数据的 6 条规矩

我手上有好几个系统在线上跑,运维只有我一个人。回头翻这几个月的记录,数据安全上出的事几乎都是同一类:以为有。

一个系统的每日加密备份早就在跑,看着很踏实。仔细一查:备份只存在那一台服务器上,而且从来没有恢复过一次。

另一个系统的一次部署,把服务器上的备份脚本悄悄转成了 Windows 的 CRLF 换行,bash 一执行就报错。这类脚本平时没人手动去跑,直到下一次部署前做备份,问题才暴露出来。

下面这 6 条规矩,就是补这些窟窿时攒下的。每条都落到一个亲眼看得到的验证动作上,而且验证要写进脚本,不能靠记性。

规矩一:定时任务装完,立刻按原样跑一次

开头那次 CRLF,是在部署前手动跑备份时才被发现的。定时任务也是同一个道理:装好不等于在跑,只有按它真正运行的方式跑一次,才知道它能不能跑起来。

最容易出事的是 cron 行末那个 >> 日志文件 2>&1。如果日志文件事先不存在,跑任务的普通用户又无权在系统日志目录里创建它,重定向就会失败;而重定向失败时,shell 根本不会执行这条命令,备份脚本连被调用的机会都没有。这时候你单独检查备份脚本,它一点问题也没有。

所以课题罗盘的部署文档里,装 cron 的那几行命令中都有两行不起眼的:先把日志文件建好,再把属主改成跑任务的用户。这两行不能省。

装完之后,再按和 cron 完全相同的方式手动触发一次,确认日志可写、备份文件真的产出了。

关键在「完全相同」:同一个用户、同一整行命令、同样的重定向。用自己的账号跑一遍备份脚本,只能证明脚本没问题,证明不了 cron 能把它跑起来。建议把这次试跑也写进部署脚本,每次部署自动做一遍。

规矩二:cron 用「装文件」,不要用管道传命令

有一条维护任务,命令里同时有变量、双引号和 &&。我试过从本机把它管道进服务器的 crontab,这要经过 PowerShell → ssh → bash → crontab 四层解释,每一层都可能吃掉一个引号或提前展开一个变量。实测直接报 bad hour;更糟的是,读取 .env 的那一步会在本机的 shell 里执行,而不是在服务器上。

正确做法是把 cron 写成文件放进仓库,用 install 装到 /etc/cron.d/。文件不经过任何一层 shell,写的是什么,装进去就是什么。

装完还要核四件事,任何一条不满足,cron 都会静默忽略这个文件,不报任何错:

  • 权限 644、属主 root;
  • 文件以换行符结尾;
  • cron 服务处于运行状态;
  • 文件名里没有点号。

还有一点:这个目录下的文件全局可读,令牌不能写进去。cron 文件里只写「运行时从 .env 读取」,令牌本身留在权限 600 的 .env 里。

规矩三:敏感变量只放 600 的 env 文件,而且只放一处

有一次切换数据库,我把带口令的连接串加进了权限 600 的 env 文件,重启后并没有生效。查下来,服务的 systemd unit 里还留着一行 Environment=,硬编码着旧的数据库地址;删掉这一行,env 文件里的值才生效。

那为什么不干脆把新连接串写进 unit?因为 unit 文件是 644,服务器上所有用户都能读,带口令的东西不能进去。

这件事让我记住两条:

  • 同一个变量只在一处定义。两处都有时谁说了算,取决于你未必记得清的规则,不如别给它选择的机会;
  • 改完要确认进程真正拿到的值,比如看启动日志里连的是哪种数据库,而不是只看配置文件写了什么。

规矩四:备份三件套——密文异地、密钥分开、恢复演练

回到开头那个系统。同一台机器上的备份只防误删,不防机器丢失。要补的是三样东西。

一是密文异地。 本机加了一个计划任务,每天把服务器上的密文备份拉到本地,本机保留 180 天,当天没开机就开机后补跑。它还顺便当了监控:连不上服务器、下载不完整,或者服务器上最新的备份已超过 26 小时(说明每日备份停了),桌面会弹窗提醒。有这道检查,每日备份一旦停了,按理第二天就会暴露。

这个计划任务自己也差点「以为有」:第一版的启动方式实测根本没拉起 bash,却报告运行成功。换掉之后,我又专门模拟了连不上服务器的情况,确认会记日志、返回失败。

二是密钥分开。 拉下来的是密文,没有密钥等于没有备份;但密钥也不能和备份放在一起,否则一台机器丢了,两样一起丢。解密密钥要单独存进密码管理器,和备份目录分开。这一步只能人工完成,脚本替不了你,所以最容易拖着不做,值得单独写进待办、做完打勾。

三是恢复演练。 演练脚本先做一份新备份,恢复进一个临时库,逐表比对条数,最后删掉临时库,正式库全程只读。2026-09-14 在生产上跑了第一次,解密和恢复都成功,14 张表的条数与正式库完全一致。

规矩五:让恢复验证便宜到每月都能做

演练能不能坚持,取决于它贵不贵。课题罗盘的恢复脚本我专门设计了一个 --verify-only 模式:把备份恢复进临时库、检查附件压缩包里的路径,不停应用、不覆盖正式库。正式恢复走的也是同一个脚本,而且会先做同样的核验,验证过的路径就是出事那天要走的路径。

节奏上,备份每天自动跑、滚动保留 14 份,每月至少挑一份跑一次验证。

另外两个小习惯:

  • 备份清单里记下代码版本(当前提交号)。恢复旧数据时,得知道它配哪一版的表结构;
  • 检查本身也要被检查。同一个系统的全库导出,表清单已经由单元测试拿数据模型逐一比对,却还是漏了一张框架自动生成的多对多连接表,因为它不算模型。两个月里导出的记录一条不少,参与者却全是空的,不报任何错。现在测试直接禁止这种隐式连接表。

规矩六:切库那天,序列和备份脚本都要跟着改

智能备课文档生成系统从 SQLite 切到 PostgreSQL 时,599 行数据、17 张非空表逐表核对一致,旧的 SQLite 文件原封不动,随时可以退回。切库也刻意单独部署,不和新功能混在一起,出了问题才好定位。

真正要命的,是两件「迁完查什么都对」的事。

一是自增序列。 迁移时显式插入了历史 id,而 PostgreSQL 的自增主键靠序列发号,显式插入 id 不会推动序列。漏掉这一步,迁完查什么都对,直到某天新建记录撞上主键冲突,那时早就看不出和切库有关了。所以迁移脚本最要紧的一步,是把每张表的序列设成 max(id)+1:

SELECT setval(pg_get_serial_sequence('表名', 'id'), COALESCE(MAX(id), 0) + 1, false) FROM 表名;

切完实测的三张表,序列的下一个值分别是 3、21、41,表内最大 id 是 2、20、40,全是 max+1。

二是备份脚本。 切库之后数据库搬进了容器卷,而部署脚本里的备份还只打包原来的数据目录,照旧跑的话,这种「备份」里根本没有数据库。我给部署脚本补上了数据库导出。换了存储,就要重新检查所有「以为数据还在老地方」的脚本。

附:自检脚本也会写脏数据

如果你的系统有接口自检脚本,要留意一件事:它每跑一次,会不会在正式库里留下一组测试数据。会的话,清库清单里单列一项「自检残留」,删完再查一遍余量是不是零。

一页纸检查清单

规矩验证动作看到什么才算过
试跑用 cron 的身份原样跑整行命令日志可写,新备份文件出现
装文件核权限属主、结尾换行、文件名、服务状态四项全部符合
权限查 env 权限,搜同名变量600,且只定义一处
异地看服务器最新备份和本机最新副本的时间都不超过 26 小时
演练恢复进临时库,逐表比对条数条数一致,正式库未写
切库查每张表序列的下一个值都不小于 max(id)+1
自检按自检数据的标记查库余量为零

给 AI 的「备份体检」提示词

Claude Code 和 Codex 都能用,在项目目录里直接丢给它:

请给这个项目做一次「备份体检」。全程只读:不改文件、不重启服务、不打印任何密钥或口令。逐项检查,附上你实际看到的命令输出作为证据:

  1. 备份由哪个定时任务触发、以什么用户运行?日志文件存在且可写吗?最近一次真正产出的备份文件是哪个、什么时间?
  2. 定时任务是以文件形式安装的吗?权限、属主、结尾换行、文件名符合 cron 要求吗?里面有没有明文令牌?
  3. 敏感变量写在哪些文件里,各自权限多少?有没有同一个变量在两处定义?
  4. 备份和解密密钥在不在同一台机器上?有没有异地副本,最新一份是什么时候的?
  5. 有没有不碰正式库的恢复验证命令?最近一次验证是什么时候?
  6. 数据库如果迁移过,每张表的自增序列是否都不小于 max(id)+1?
  7. 自检或测试脚本会不会往正式库写数据?

没验证的写「未验证」,不要推测。最后按风险从高到低列出要补的项。

小结

一个人管线上数据,最怕的不是出事,而是「以为有」:以为备份在跑,以为能恢复,以为配置生效了。这 6 条规矩说到底是一件事:把每一个「以为」换成一次亲眼看到的验证,再把这次验证写进脚本。 没恢复过的备份,不算备份。

← 返回指南列表