给运维脚本补上退出码、锁和重试
一批备份脚本是早期用交互方式一点点拼出来的。在终端里一步步跑没问题,放进定时任务之后就开始出现两种事故:失败了但任务显示成功,以及两次任务叠在一起,把同一份文件写坏。
失败就应该是非零退出
脚本开头加上:
1 | set -euo pipefail |
set -e 让命令失败时脚本停下来。set -u 让使用未定义变量时直接失败,而不是把空字符串拼进路径。pipefail 让管道中间的失败也能传递出来,否则 cmd | tee log 只会看 tee 的退出码。
有一个脚本把备份拷到远端,拷贝失败后仍然去删除本地七天前的文件,因为删除命令成功了,整个脚本的退出码就是 0。定时任务因此显示绿色。把删除放在拷贝成功之后,并在失败时直接退出,任务状态才和实际结果一致。
同一时间只跑一份
备份一旦超过定时周期,下一轮会和上一轮同时写同一个目录。我用文件锁把并发收成排队:
1 | exec 9>/var/lock/backup.lock |
拿不到锁就退出,并记一条日志说明跳过的原因。这里选择退出而不是等待,是因为重叠通常说明上一轮已经超时,继续排只会越积越长。这种情况应该告警,而不是默默排队。
重试要有上限和间隔
对远端拷贝加重试是合理的,网络抖动不该让整天的备份失败。重试三次,间隔 15 秒、30 秒、60 秒。不要在循环里无限制重试,也不要在失败后立刻重试把对端打满。
重试只包在拷贝这一步。不要把「删除旧文件」也放进重试循环,否则一次逻辑错误会被重复执行。
再跑一次也要安全
脚本可能在拷到一半时被中断。再次执行时,目标位置如果已经有半份文件,应该换一个临时名字写入,完成后再改名。改名是同一文件系统上的原子操作,读的一方不会看到半份内容。
我用日期和进程号组成临时文件名,避免两次运行撞在一起。完成标记单独写一个小文件,恢复时先看标记,而不是靠文件大小猜测是否写完。
这些改动加起来不到四十行,但定时任务的结果从此可以相信:绿色就是写完了,红色就是没写完,而不会是两种状态混在同一次运行里。