Research

定时任务漏跑后,到底该补什么?

从触发补偿、数据区间与副作用三层分析漏跑后的处理边界,比较 systemd Persistent timer 与应用层水位。

为《定时任务漏跑后,到底该补什么?》确定性生成的文章图形
图形生成说明
函数家族
harmonic-envelope
简化公式
y(t) = Σ aₙ·sin(nωt + φₙ) / nᵖ
短哈希
68468bdb8859
生成器版本
2

这是由文章身份与结构生成的装饰图形,不是数据可视化。

下载 SVG下载生成清单

定时任务“漏跑”常被当成调度器是否可靠的问题,但这只回答了最靠前的一层:某个进程有没有被拉起。真正要补的,可能是一条通知、一个业务日、一批输入,或者什么也不该补。把这些对象混在一起,最常见的结果是开机后补跑一次看似正常,数据区间却仍有洞,或同一笔副作用被写了两遍。

这篇文章把“补跑”拆成三个可分别判断的问题:是否补一次触发、是否补齐时间区间、以及能否承受重复执行。这里讨论的是设计推演,不是某个生产系统的事故复盘,也不把下面的规则当作可直接套用的配置模板。

先确认漏掉的是什么

设任务按“包含起点、不含终点”的连续时间区间处理输入,机器停机期间错过了几个区间。恢复后启动一次任务,只能证明有一次新的执行;它并没有说明执行参数覆盖了哪些区间,也没有说明上次执行是否已经把部分结果提交出去。

因此,排查的起点不该是“几点没有运行”,而是三个较具体的对象:最后一个已确认完成的业务区间、允许补到多晚的截止点、以及该区间的结果是否可以重复写入。只要求当前快照的备份、临时文件清理和缓存预热,可能只需恢复后运行一次;要求历史恢复点的备份、日报和数据同步,则还要核对缺失区间。后一类任务若没有明确的区间边界,调度器无法替应用推断遗漏范围。

Persistent 补的是一次触发

systemd 的 Persistent=true 可为带 OnCalendar= 的 timer 记录上次触发时间;若本应触发的时间落在 timer 未激活期间,timer 再次激活后会尽快启动对应服务。systemd.timer 手册 对此的描述很清楚,但它的语义是“触发关联服务”,不是把离线期间的每个时点逐一重演。

这很适合“恢复后做一次检查”一类工作。例如某项维护只需确认当前状态,补做一次比补做七次更合适。但若服务内部默认取“现在”,周一恢复时跑一次日报并不会自动补齐周末两天;若服务内部默认取“上次成功时间”,又必须解释上次运行在提交一半时中断的情况。Persistent 保存的是调度层的历史,并不是业务完成水位。

Persistent=true 当作“不会漏数据”的承诺,会掩盖这条分界。它解决的是宿主机暂时关闭或 timer 未运行后的启动机会,不能验证远端接口是否可用、输入是否仍可取得,也不能撤销已发生的外部动作。

应用层需要持有水位与提交边界

对区间型任务,一个较稳妥的模型是把“已完成至何处”存成可审计的水位。每次运行先读取水位,生成一个有限的待处理区间;完成处理、校验结果并写入水位,应尽量构成同一可恢复的提交边界。失败时宁可保留旧水位,让下次重新取得该区间,也不要在工作开始时先把水位推进。

这并不自动得到“恰好一次”。写库与发消息或删除远端对象未必落在同一个原子事务里。可行的设计方向是让接收方按业务键去重,并保存每个区间的运行记录;仍需核实去重记录与业务效果是否一起提交。只在发送方记一句“已发送”,解决不了接收成功后、记录写入前断线的窗口。这里的水位模型也以输入完整、区间可判定为前提;迟到或被源端修订的数据,需要另设回看和修订规则。

一个反例是每天只发送一封提醒邮件。用水位补齐三天,可能比漏发更扰人;这种任务应明确选择“不补”或合并为一则恢复摘要。相反,按日拉取公开数据时,若源端保留历史,水位应驱动逐日追赶;若源端只保留最近窗口,则先记录无法恢复的区间,比悄悄用今天的数据填过去更诚实。

调度器仍然需要给出边界

应用层有水位,不意味着调度器可以不设限制。长时间运行的任务可能与下一次重叠,恢复后的积压也可能压垮依赖方。Kubernetes 的 CronJob 文档把这个问题明确拆成延迟启动截止时间和并发策略:Forbid 会跳过与仍在运行任务重叠的时点,Replace 则终止前一任务;文档同时提醒,调度在某些情况下可能出现重复或缺失,因此 Job 本身应具备幂等性。CronJob 文档 的这个约束虽来自另一套调度器,却说明了同一个事实:调度记录不是执行保证。

systemd 也不负责替服务判定“完成”。服务的退出状态只说明其进程如何结束;对于多阶段任务,应用应把可观察的成功条件放在自己的记录或产物校验中,而不要只以进程返回零作为数据完整性的证据。systemd.service 手册 对服务生命周期和退出结果的定义,正好限定了这一层责任。

一个可审查的取舍顺序

处理漏跑时,先列出业务区间和允许的迟到窗口,再决定恢复后是跳过、合并一次,还是逐区间追赶;随后才选择 timer 的持久触发和防重叠策略。最后为每次区间留下开始、结束、输入版本、结果和水位变化。这样做的成本是多一份状态和复核面,但换来的是可以回答“补了什么、为何不补其余部分、重复会怎样”的证据。

如果任务没有区间语义且副作用可接受,Persistent=true 往往已经足够;只要任务对完整性、重复或时效有要求,补偿判断就应落回应用层。两者不是替代关系:前者负责在机器恢复后提供一次执行机会,后者定义那次机会究竟该完成什么。