Linux 上的报表脚本每分钟启动一次,偶尔却要跑几分钟:上一轮还没结束,下一轮已经来抢同一份输出。加了“锁文件存在就退出”仍可能撞车。下面用 util-linux 的 flock 做单机互斥,拆清锁冲突、业务失败和锁文件残留,并用隔离实验验收。这里讨论长期有效的排障方法,不是某个版本的新功能。
一、先确认:要互斥,不是要排队
先核对调度入口、运行用户、任务耗时与输出对象。cron、手工执行、运维平台可能同时启动一个脚本;只给其中一个入口加锁,另一条路仍然畅通。互斥应放进共同的启动包装脚本,而不是靠操作人员记住规则。
本文适用于同一台 Linux 主机、本地文件系统、所有参与者主动使用同一个锁文件的场景。flock 通常是建议锁,不会禁止绕过锁的程序直接写业务文件。跨主机、多容器独立文件系统或网络挂载,不能照搬成分布式锁。
周期性刷新通常允许“上轮未完成,本轮跳过”,用 -n;如果必须等一会儿,用 -w 10 限制等待。默认阻塞可能让进程越积越多。每条任务都必须处理时,应考虑持久队列与任务状态,而不是让 cron 排长队。

二、锁住的是打开的文件,不是一个名字
flock 的锁关联到打开文件描述(open file description)。通过 fork 或 dup 派生的描述符可能共享这把锁;显式解锁或相关描述符全部关闭后,锁才释放。进程结束后磁盘上仍有空的 job.lock,不代表锁还在。
不要在运行中删除锁文件“解锁”。旧进程可能仍持有原来的 inode,后来者重建同名文件后得到另一个 inode,两边反而都能拿到锁。锁文件应长期保留,且放在运行用户控制、其他用户不能随意替换的目录里。单独检查文件是否存在,再创建文件,也不是原子抢锁。
三、把加锁放进统一入口
先确认安装的是 util-linux flock,并检查本机是否支持所用选项:
command -v flock
flock --version
flock --help下面保存为包装脚本。路径是示例,需替换为真实业务脚本;同一任务的所有入口使用同一运行用户、同一 HOME 和同一锁路径。
#!/bin/bash
set -eu
umask 077
lock_dir="$HOME/.local/state/report-job"
mkdir -p "$lock_dir"
exec 9>>"$lock_dir/job.lock"
if flock -n -E 75 9; then
:
else
rc=$?
if [ "$rc" -eq 75 ]; then
printf '上一轮仍在运行,本轮跳过\n' >&2
else
printf '加锁失败,退出码:%s\n' "$rc" >&2
fi
exit "$rc"
fi
exec /bin/bash /opt/example/report.sh文件描述符 9 在 exec 后继续传给前台业务脚本。业务不要主动关闭或解锁它,也不要立即转后台让父进程退出。包装器不吞掉业务退出码;75 在“加锁分支”表示竞争失败,但业务本身也可能返回 75,所以告警应结合阶段日志,不能只盯一个数字。
这里用 if 接住加锁失败,避免 set -e 提前退出。不要改成 if ! flock ... 后再读 $?:那时读到的是取反后的状态。目录权限仍要事先确认;umask 不会修复已有的不安全目录。
四、接入 cron 前先核对环境
包装脚本统一命名为 /opt/example/run-report.sh 后,可参考下面的当前用户 crontab 条目;不要把它直接粘进系统 crontab,后者还需要用户字段。
* * * * * /bin/bash /opt/example/run-report.sh上线前记录一次正常耗时,并给任务超时设置合理告警。加锁不会让慢任务变快,也不会替你修复数据库等待;如果每轮都被跳过,应回到业务耗时本身定位原因。
cron 的 PATH、HOME 与交互终端可能不同。确认运行身份和 HOME 后,把 flock 改为 command -v flock 查到的绝对路径,并显式配置业务依赖的 PATH。日志目录也要预先准备并设置轮转。包装器返回 75 可以定义为可观测的跳过,但持续跳过意味着任务耗时或卡住,不能永久静默。
还可用 findmnt -T "$HOME/.local/state/report-job" 检查锁所在挂载。NFS、CIFS 的锁行为依赖内核、协议与挂载选项;本文的本地验证不能替它们背书。
五、用隔离实验验证两轮不能同时进入
不要拿生产脚本做竞争实验。先在自有临时目录准备一个前台测试任务:进入后写下标记,等待测试端放行,再结束。启动第一份包装器,看到进入标记后,再启动第二份。
本次在隔离夹具中替换了 HOME 和业务路径,执行上述包装器:第一份运行时第二份返回 75,第二份没有写入业务标记;第一份结束后第三份可以进入。另测业务返回 23,包装器也返回 23。测试没有安装 cron,也没有修改真实业务。
还要覆盖“看着异常,实际上正常”的情况:第一份退出后保留锁文件,再次启动应成功;人为在测试目录删除并重建同名锁文件,则两个不同 inode 可以分别持锁。这是解释误用的负例,不是建议在线上尝试。
六、卡住时检查描述符继承
主脚本已经结束,新任务却总是竞争失败,可能有后台子进程继承了描述符。先查进程树和打开文件,而不是删锁。若你选择命令包装形式,flock 提供 --close,可在执行业务命令前关闭传给子进程的锁描述符,由外层 flock 持锁等待。
但 --close 不是通用补丁:业务一旦启动后台任务后立即退出,外层就可能释放锁,后台工作却仍在继续。最稳的约束仍是业务以前台方式完成;确需后台并发时,由主进程等待所有需要保护的子任务。不要混用 --no-fork 和 --close,两者不兼容。
七、按症状走分支,别一上来就删文件
| 现象 | 优先核对 | 处理方向 |
|---|---|---|
| 两份都执行了 | 用户、HOME、路径、inode、入口 | 统一入口;停止替换锁文件 |
| 立即返回非 75 | 目录权限、flock 选项、打开文件错误 | 保留原始错误,勿归类为竞争 |
| 持续返回 75 | 业务耗时、进程树、描述符继承 | 定位持锁者,按业务流程处理 |
| 锁文件在,但任务可启动 | 是否确有持锁进程 | 正常保留文件,不做定期清理 |
| 单机正常,多机撞车 | 是否共享可靠的协调机制 | 改用队列或适合业务的分布式协调 |
八、上线验收不止看“没有重复进程”
- 两个入口竞争时,只有一个进入业务区,另一个留下明确跳过记录。
- 第一轮完成后能再次获得锁;业务失败的退出码仍被保留。
- 锁文件不被轮转、清理或部署替换;业务期间没有过早关闭持锁描述符。
- 所有受保护的子任务结束后才释放锁;告警区分跳过和真实失败。
- 重复提交、崩溃重试等场景另做幂等校验:互斥只能减少并发,不能保证业务“恰好一次”。
参考:util-linux flock 命令手册;Linux man-pages flock(2)。本文含 AI 辅助整理,命令验证范围如上,不代表在所有文件系统上测试通过。
🔕 评论已关闭