定时任务的六个可靠性习惯:Persistent、flock 与结构化结果
背景
把一个脚本挂成每日定时任务很容易,难的是让它连续几个月不出错:机器重启错过执行点、任务跑超时后上一轮还没结束就开了下一轮、日志无限膨胀把根分区写满。这三件事我都遇到过,最后用 systemd timer + flock + 一个小而固定的日志策略解决。
一、timer 的 Persistent 要打开
systemd timer 默认是「按墙钟触发」,机器关机的那段时间直接跳过。加一行 Persistent=true,systemd 会记录上次触发时间,开机后如果发现错过了,会立刻补跑一次。对签到、拉报表这类「补跑有意义」的任务,这一行是刚需。
[Timer]
OnCalendar=*-*-* 00:30:00
Persistent=true
RandomizedDelaySec=300
顺手加个 RandomizedDelaySec:同一批机器都盯着 00:30,不加抖动的话会在整点把上游接口打出一小波峰值。
二、flock 放在 ExecStart 里,不要放脚本里
很多人习惯在脚本开头自己写 pid 文件判断,但异常退出留下的 stale pid 会让后续每一次执行都被自己挡住。systemd 场景下更省事的做法是把互斥交给 flock:
ExecStart=/usr/bin/flock -n /run/myjob.lock /usr/local/bin/myjob.sh
-n 是关键,表示拿不到锁就立刻失败退出,而不是排队。排队会带来一个更隐蔽的问题:任务迟到几小时后突然集体开跑,行为和你白天调试时完全不同。让它在日志里明确写一条「locked, skip」比排队安全得多。
配对地,unit 里最好加上:
[Service]
Type=oneshot
TimeoutStartSec=1800
KillMode=control-group
KillMode=control-group 保证超时后连子进程一起收掉,否则留下的孤儿进程会继续占着锁和端口,下一轮又会因为「锁被占」静默跳过,形成「日志里天天说跳过、实际什么都没做」的假象。
三、日志:让 journald 管,不要让脚本自己写
我早期的写法是脚本里 console.log 一份、run.sh 里再重定向一份,结果同一个文件里每行都出现两遍,排查时还以为是任务被触发了两次。现在统一成脚本只往 stdout 写,由 journald 收:
journalctl -u myjob.service --since today --no-pager
需要长期留档再单独配 SystemMaxUse= 限额,而不是让脚本自己 roll。原因很简单:自己 roll 的日志在崩溃时最容易丢,而崩溃恰恰是最需要日志的时候。
四、把「执行结果」结构化落盘
日志是给人看的,报告是给程序看的。我的做法是任务结束时写一个固定 schema 的 JSON:时间戳、每个子任务的 OK/FAIL、失败原因、关键数值的前后对比。邮件或告警渲染器只读这个文件,不再去 grep 日志。
这么做的好处是「报告」和「执行」解耦:任务崩了也能发一封说明崩在哪一步的信;而渲染脚本可以单独用 --dry-run 跑,不用真去触发任务。踩过一次坑:渲染器原来只看 errors 数组,任务级的失败写在 actions 里,于是每天都发「全部成功」的假喜报。后来把两条路径统一成同一份结构化数据,这类误报才彻底消失。
五、开工前的环境自检要写进脚本
定时任务失败的绝大多数原因不是业务逻辑,而是环境:依赖的服务没起、凭据过期、某个外部通道断了。这些检查如果写在文档里靠人记,一定会漏。写成脚本里的前置断言,失败就带明确原因退出,比事后翻日志便宜得多。
小结
无人值守任务的可靠性,靠的不是某一个技巧,而是几条固定习惯:timer 打开 Persistent 且加抖动、互斥交给 flock 并保证 -n、超时用 control-group 收干净、日志交给 journald、结果落结构化 JSON、开工先做环境断言。这六条加起来不到二十行配置,却能把「每天要人看一眼」变成「只在真出问题时才看」。
- 点赞
- 收藏
- 关注作者
评论(0)