定时任务别总踩整点:systemd timer 的随机化、补跑与它埋的坑

举报
yd_242458115 发表于 2026/10/30 10:42:20 2026/10/30
【摘要】 AccuracySec、RandomizedDelaySec、Persistent 三者的区别,以及修改 OnCalendar 时被判定为「错过」而立即补跑的真实踩坑,附排查与规避方法。

先说结论

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,放弃合并唤醒

最后一句:定时任务的"整齐"是一种可以被外部观察到的特征。如果任务本身没有严格的时序要求,适度的抖动既省资源,也更自然。

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

评论(0)

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

全部回复

上滑加载中

设置昵称

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

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

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