用 cron + flock 避免定时任务重叠执行:一个备份脚本的教训
一、事故:备份跑着跑着就重叠了
有一次,一台服务器上的备份脚本被 cron 设为每小时执行一次。平时数据量不大,几分钟就跑完,一直没出问题。后来数据量变大,单次备份耗时超过一小时,上一轮还没结束,下一轮又启动了。两份 rsync 同时往同一个目标目录写,临时文件互相覆盖,日志里先是报错,最后连哪一份备份是完整的都说不清。那次之后我才把一条规则写进运维清单:定时任务必须先防重叠,再谈优化速度。
二、重叠执行到底会带来哪些危害
第一,写冲突。两个进程同时写同一份目标文件或同一个快照目录,结果可能是半成品,甚至损坏。第二,资源争抢。备份、压缩、传输都会吃 CPU、内存和磁盘 IO,并行跑只会让两轮都更慢,形成恶性循环。第三,状态混乱。清理旧备份的逻辑如果被两轮同时执行,可能删掉正在生成的新备份。第四,排障困难。日志交错在一起,很难判断某条报错属于哪一轮。
三、flock 的基本用法
flock 是 util-linux 自带的命令行文件锁工具,大多数 Linux 发行版默认就有。基本思路是:给一个锁文件加排他锁,拿到锁才能执行,拿不到就退出。
最常用的写法有两种。第一种是用 flock 直接包装命令,锁在命令执行期间持有,命令结束自动释放:
flock -n /var/lock/backup.lock -c '/usr/local/bin/backup.sh'
第二种是在脚本内部打开文件描述符加锁,适合脚本本身比较长、需要多处判断的场景:
exec 200>/var/lock/backup.lock
flock -n 200 || { echo "上一轮备份仍在运行,本轮退出"; exit 1; }
关键参数要分清:-n 表示非阻塞,拿不到锁立刻退出,不排队等待;-w 60 表示最多等 60 秒,超时再退出;不加 -n 也不加 -w 时会一直等待,这在 cron 里往往不是想要的效果,因为排队的任务会越积越多。
四、一个带 flock 的 cron 备份脚本示例
下面是一个可以直接用的示例。把源目录、目标目录改成自己的路径,先手动跑一遍确认无误,再交给 cron:
#!/bin/bash
set -euo pipefail
SRC="/data/www"
DEST="/backup/www"
LOCK_FILE="/var/lock/backup-www.lock"
LOG_FILE="/var/log/backup-www.log"
# 打开锁文件并尝试加锁,拿不到锁就退出,绝不重叠执行
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
echo "$(date '+%F %T') 上一轮备份仍在运行,本轮退出" >> "$LOG_FILE"
exit 0
fi
# 锁住之后再写进程号,方便排查是谁持有锁
echo $$ 1>&200
echo "$(date '+%F %T') 备份开始" >> "$LOG_FILE"
rsync -a --delete "$SRC/" "$DEST/" >> "$LOG_FILE" 2>&1
RSYNC_STATUS=$?
if [ "$RSYNC_STATUS" -ne 0 ]; then
echo "$(date '+%F %T') 备份失败,rsync 退出码:$RSYNC_STATUS" >> "$LOG_FILE"
exit "$RSYNC_STATUS"
fi
echo "$(date '+%F %T') 备份完成" >> "$LOG_FILE"
对应的 cron 配置,每小时执行一次:
0 * * * * /usr/local/bin/backup.sh
注意:不要在 cron 里再包一层 flock,也不要在脚本里重复加锁,两层锁容易把问题藏起来。锁只加一处,逻辑才清晰。
五、锁文件位置与失败处理
锁文件建议放在 /var/lock 或 /run/lock 下,这是系统预留给锁文件的目录,重启后一般会被清理,不会因为旧锁文件残留而误判。不要把锁文件放在备份源目录或目标目录里,避免被 rsync 同步或清理逻辑误删。
失败处理要分三种情况。第一,拿不到锁:这是正常保护,不是错误,建议记一条日志后以 0 退出,避免 cron 发无意义的告警邮件;如果希望被告警,就明确返回非零并在日志里写清原因。第二,脚本中途失败:用 set -euo pipefail 让错误尽早暴露,rsync 的退出码要单独保存和判断,不要被后续命令覆盖。第三,进程被杀导致锁残留:flock 是基于文件描述符的锁,进程退出后内核会自动释放,不需要手动删锁文件;如果发现锁一直被占用,先用 fuser 或 lsof 查持有进程,不要直接删文件了事。
六、验证方法
验证分四步。第一步,手动执行一次脚本,确认能正常完成,并在日志里看到开始和完成两条记录。第二步,开两个终端,同时执行脚本,第二个应该立刻退出并记录“上一轮仍在运行”。第三步,用一个故意 sleep 很久的测试脚本模拟长任务,在它运行期间再触发一次,确认不会重叠。第四步,检查锁占用情况:
fuser -v /var/lock/backup-www.lock
lsof /var/lock/backup-www.lock
能看到持有进程,说明锁确实在工作。最后再把 cron 的执行记录对一遍,确认每小时只有一条开始记录,没有成对出现的重叠记录。
七、总结
cron 只负责准时触发,不负责判断上一轮是否跑完。防重叠是脚本自己的责任,而 flock 是成本最低的做法:一个锁文件、一个 -n 参数,就能把“备份跑重了”这类事故挡在门外。先保证同一时间只有一个实例在跑,再去优化备份速度和保留策略,顺序不能反。
- 点赞
- 收藏
- 关注作者
评论(0)