用 cron + flock 避免定时任务重叠执行:一个备份脚本的教训

举报
yd_226757564 发表于 2026/10/29 10:06:55 2026/10/29
【摘要】 定时备份跑重了会互相覆盖、争抢资源。用 flock 给备份脚本加一把非阻塞锁,拿不到锁就退出,配合 /var/lock 锁文件与日志记录,就能避免 cron 任务重叠执行。

一、事故:备份跑着跑着就重叠了

有一次,一台服务器上的备份脚本被 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 参数,就能把“备份跑重了”这类事故挡在门外。先保证同一时间只有一个实例在跑,再去优化备份速度和保留策略,顺序不能反。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。