定时任务别总踩整点:systemd timer 的随机化、补跑与它埋的坑
先说结论
systemd timer 有三个参数决定了它"像不像人:
AccuracySec:允许提前或延后多久触发,默认 1 分钟RandomizedDelaySec:每次触发时再随机延迟一段Persistent=true:错过的时间点要在启动后补跑
前两个决定"准不准时",第三个决定"漏了要不要补"。它们看起来互不相干,但组合出错时会出现很反直觉的行为——比如改了一行日历表达式,服务立刻被执行了一次。
一、AccuracySec 只是"合并窗口"
systemd 默认 AccuracySec=1min,意思是实际触发时间允许落在目标时刻前后的窗口内。这个窗口原本是为了省电:把同一时刻的多个 timer 合并成一次唤醒。
也就是说,即使你不做任何配置,触发时间本来就不是精确的整分。但窗口最长只有 1 分钟,从外部看仍然是"每分钟一次"的整齐节奏。
注意:AccuracySec=0 会让触发变得精确,代价是失去合并唤醒的节能效果。除非时序要求严格,一般不需要这么做。
二、RandomizedDelaySec 才是真正的打散
RandomizedDelaySec=N 表示每次触发后再随机等 0 到 N 秒才真正启动服务。
它的作用是让"节奏抖动"变得可观。举例:
[Timer]
OnUnitActiveSec=5min
AccuracySec=30s
RandomizedDelaySec=90s
实际间隔就变成 5 分到 6 分半之间的随机值。如果这是每 5 分钟一次的库存查询,外观上就不再是整齐的等间距采样。
对 OnUnitActiveSec 这种"上次激活后 N 时间"的相对定时器,RandomizedDelaySec 是在相对时间之后再叠加,不会累积漂移出太大偏差。
还有一个小细节:RandomizedDelaySec 和 AccuracySec 是叠加关系,不是替代。所以想彻底打散,两个都要设,且 RandomizedDelaySec 要明显大于 AccuracySec。
三、Persistent=true 的补跑是一个坑
Persistent=true 的语义是:如果这个定时器应该触发时系统没开机(或服务没跑),那么系统恢复后补跑一次。
它的实现不是记住"上次触发时间",而是记录一个时间戳文件,下次启动时对比:如果"应该触发的时刻"晚于已记录的时间戳,就判定为错过,立刻执行。
问题就出在这里——每一次配置变更、每一条计时器重新加载,都会被当作一次"恢复"。如果当天的触发点已经过去,而记录的时间戳比较旧,systemd 就会认为"错过了一次",当场补跑。
四、一次真实的踩坑过程
我原本有一个每天 00:02 执行的批处理任务,想改成"每天上午 7 点触发,再随机延迟几小时",于是做了三件事:
# 1. 改 timer 的日历表达式
OnCalendar=*-*-* 07:00:00
# 2. 把 service 的 ExecStart 换成一个先 sleep 随机秒数再执行的包装脚本
ExecStart=/path/to/run-random.sh
# 3. 重新加载并重启 timer
systemctl daemon-reload
systemctl restart my-daily.timer
结果 restart 之后不到一秒,服务就启动了。
原因:当天 07:00 早已过去,Persistent=true 加上新的 OnCalendar 让 systemd 判定"今天这一次错过了",于是立刻补跑。而我期望的是"明天 07:00 才开始生效"。
排查方法很简单,看两个地方:
systemctl list-timers 'my-*' --all --no-pager
systemctl status my-daily.service --no-pager -n 10
list-timers 的 LAST 列会显示"刚刚",而 status 里能看到 service 处于 activating。
五、怎么避免
三条经验:
第一,改日历表达式之前先想清楚会不会被判定为错过。 如果任务本身就允许一天执行一次,立刻补跑未必有害;但如果是"每天发布一次"这类有幂等性要求、或者会消耗额度的任务,补跑就是实打实的损失。
第二,临时关掉补跑再改。 修改期间可以先 Persistent=false,重启 timer 之后再改回 true:
sudo sed -i 's/^Persistent=true/Persistent=false/' /etc/systemd/system/x.timer
sudo systemctl daemon-reload && sudo systemctl restart x.timer
# 改 OnCalendar ...
sudo sed -i 's/^Persistent=false/Persistent=true/' /etc/systemd/system/x.timer
sudo systemctl daemon-reload
第三,包装脚本里的 sleep 要做上限保护。 如果随机延迟可能是 4 小时,而 service 设置了 TimeoutStartSec=600,那服务会在 sleep 中途被杀掉。要么把 TimeoutStartSec 调到大于最大延迟,要么把 sleep 逻辑挪到脚本外(比如用 ExecStartPre 且整体放宽超时),要么干脆把随机化交给 RandomizedDelaySec。
这一条特别容易忽略,因为它不会立刻报错——只会表现为"任务偶尔没跑"。
六、什么时候该用哪个
需求 | 用什么
同一时刻多个 timer 想合并唤醒 | AccuracySec(默认即可)
让触发时刻每天不固定 | RandomizedDelaySec
执行时间需要跨越几个小时随机 | 包装脚本 sleep,或把 timer 周期调大
机器可能关机,漏了要补 | Persistent=true,但要接受补跑语义
必须精确到秒 | AccuracySec=0,放弃合并唤醒
最后一句:定时任务的"整齐"是一种可以被外部观察到的特征。如果任务本身没有严格的时序要求,适度的抖动既省资源,也更自然。
- 点赞
- 收藏
- 关注作者
评论(0)