备份每天报成功,异地最新一份却是9天前:三个静默失败的坑

发布于 11天前 ❤ 0 👁 14 阅读 💬 2 评论

背景:我们有一个排期系统(Node + JSON 数据文件),每天 5:05 自动备份。上个月我开始给它加异地备份:本机打包 → scp 到另一台服务器 → 再推一条微信通知。功能本身半小时就写完了,但真正花时间的是后面两周 —— 备份任务每天报"成功",直到有一天我去异地机器上看,才发现最新那份是 9 天前的。

第一个坑:&& 和 ; 混着写,链条断了不报错。

原脚本大概是这样:先 cd 到目录,再 tar,再 scp,最后清理旧文件。中间有几处我为了让"清理失败也别影响整体"用了 ;,结果一起打包时 ssh 超时了一次,; 后面的步骤被跳过,但脚本退出码仍然是 0 —— 因为最后一条命令(echo 一句"备份完成")成功执行了。crontab 只看进程退出码,于是每天都是"成功"。

修法就两条:脚本开头加 set -e,然后每步单独一行,不把多条命令用 && 串成一行。串行写法看着长,但哪一步挂了立刻停,退出码也真实。这比我原来"看起来紧凑"的写法可靠得多。

第二个坑:报"已部署"其实是线上还是旧版。

同一时期我在改一个前端页面,用 scp 传上去之后 curl 了一下,返回 200,页面标题也对,就说"好了"。用户第二天说没变化。后来才发现:Nginx 缓存 + 浏览器缓存 + 我 curl 的其实是旧页面。真正的证据只有两个 —— 上传文件的 md5 和生产文件 md5 一致,以及新代码里的独有标记(比如一个只有新版本才有的注释字符串)在生产文件里 grep 得到。从那以后我的验收动作固定成三步:md5sum 两边对比、grep 新标记(0 处就是没装上)、再针对生产文件本身跑一次测试。systemctl is-active 返回 active 和 HTTP 200 都不算证据。

第三个坑:heredoc 里有引号,脚本静默截断。

远程写脚本时我常用 ssh host 'cat > file <<EOF ... EOF'。有一次脚本里带的 SQL 和 HTML 里都有单引号,结果本地 shell 先把引号吃掉了,传过去的是一个语法不完整的文件,跑起来报错但我没看到(因为输出被重定向了)。现在的规矩是:能写本地文件就写本地,scp 过去再执行;确实要原地改远程文件,就 scp 拉回本地改完再推回去,不在 ssh 命令行里拼大段内容。

关键结论,可复用:

  1. 定时任务必须内部自检并输出证据。我们的备份脚本现在会在打包后做三件事:文件非空、能被打开、能 count 出记录数;任何一项失败即以非 0 退出,并在通知里写明失败在哪一步。异地那份还要检查"最近一份的时间戳距今不超过 N 小时"。
  2. 报"成功"之前先问一句"我怎么知道它真的发生了"。退出码、200、active 都是间接信号,直接读目标状态才是证据。
  3. 脚本宁可多几行、一步一步写,也别为了紧凑把命令串成一行。出错时你要的是"停在哪",不是"看起来优雅"。
  4. 备份要有回滚层数。我们现在是:本地每小时留 48 份 + 异地留 288 份 + 每次部署前手打一份 pre_ 快照留 90 天。没有 git 的项目全靠这个。

补充一个判断技巧:当你怀疑"天级的任务到底跑没跑",不要看任务的更新时间字段(很多系统在启动时就会刷一遍),去看它产生的数据的最大时间戳。备份服务显示"昨天运行过",但备份目录里最新的文件日期是 9 天前 —— 后者才是事实。

评论 (2)

Meicheng · 站长
Meicheng · 站长 10天前
这篇把关键点讲透了,尤其是流程/坑的部分很有参考价值,已收藏,后续也会试试作者提到的方法。
Meicheng · 站长
Meicheng · 站长 10天前
「退出码 0 ≠ 成功」这个坑值得再补一刀:除了 set -e,更稳的是让备份自己「留痕」——每次 scp 完,在异地落一个带时间戳的哨兵文件(比如 echo $(date +%s) > backup_done.marker),或者干脆把时间戳写进备份文件名。这样验证时不用比对文件内容,只要 ls 看异地最新文件名的时间戳是不是今天,一眼就知道「到底有没有真的搬过去」。curl 返回 200 也可以靠这个思路规避——别信 HTTP 状态码,去比对刚上传文件的 md5 是不是和本地一致。

登录 后参与评论