TT Lab
开始
学习 学习路径 课程

日志来自未来 — 时钟制造的五起事故

本该只跑一次的结算跑了两次

在 TT Lab 中继续学习

一句话总结

周期性任务无法用“跑了几次”来判断是否正常。被跳过的一次和多跑的一次会互相掩盖,使总数恰好吻合。

为什么需要它

对定时任务的监控,通常只看两件事:是否失败,以及跑了几次。这两项都抓不到的事故,是本课程的最后一块拼图。

夏令时结束的那天凌晨,本地时间 01:00 的结算会跑两次。两次都成功,所以没有失败告警。夏令时开始的那天凌晨,02:00 的结算则一次都不会跑。因为什么都没发生,所以同样没有告警。而把这两天的执行次数加起来,与预期值完全相同。统计总数的监控,在结构上就抓不到这类事故。

工作原理

问题的根源在于任务要处理的区间的命名方式。如果像“01 点结算”这样用本地时间命名,本地时间出现两次的那天,同一个名字会出现两次,而本地时间不存在的那天,一次也不会出现。如果用时刻(UTC)命名,每个名字始终只会出现一次。

调度器一侧也做出了同样的选择。Kubernetes 的 CronJob 通过 .spec.timeZone 来指定按哪个时区的时间解读调度计划(从 v1.27 起稳定),不指定时,则按 controller manager 的本地时间解读。明确写上 Etc/UTC,就与夏令时无关了。顺便一提,在调度字符串中加入 TZ 或 CRON_TZ 的做法官方并不支持,加入后创建资源会因校验错误而被拒绝。

但是,即使把时区固定为 UTC,“恰好一次”也无法得到保证。同一份文档是这样写的:CronJob 对每个调度时间点大约创建一次 Job,有时会创建出两个,有时一个也不会创建,Kubernetes 会尽量避免这种情况,但无法完全杜绝。所以文档明确要求你定义的 Job 必须是幂等的。这也是本课程的最后一句话。

重叠则由另一个旋钮来处理。.spec.concurrencyPolicy 接受三个值。

Allow    (기본) 앞 실행이 안 끝났어도 새 실행을 만든다
Forbid   앞 실행이 안 끝났으면 이번 실행을 건너뛴다
Replace  앞 실행을 새 실행으로 갈아 끼운다

该代码块中的韩文注释依次说明:Allow(默认)即使上一次执行尚未结束,也会创建新的执行;Forbid 上一次执行尚未结束时,跳过本次执行;Replace 用新的执行替换上一次执行。

此外,.spec.startingDeadlineSeconds 规定了最晚可以比预定时间晚多久开始。超过这个值仍未能开始的执行会被跳过,Kubernetes 会把它当作失败的 Job 处理。文档还另外给出两条警告:如果把这个值设得小于 10 秒,由于控制器每 10 秒检查一次,任务可能根本不会被调度;如果错过的调度超过 100 个,控制器会放弃启动。

Linux 一侧的 systemd 定时器也有同样的区分。它分为对齐挂钟时间的 OnCalendar,以及按经过时间计数的 OnUnitActiveSec 系列,前者在修改时钟后会重新计算下一次触发时间。

在现场相遇的样子

真正实现幂等性的方法,通常是运行键。构造一个能唯一指明这次执行处理的是什么的字符串,如果用这个键查到已有处理记录,就不再重复写入。选择键的标准只有一条:同一件事做两次,必须得到相同的键;不同的事,必须得到不同的键。如果把执行 ID 或开始时间这类每次执行都会变化的值放进键里,跑了两次的任务就会被当成两次都是新任务。这种错误其实最为常见。

防止重叠与幂等性是两个不同的问题,这一点也经常被混淆。防止重叠,是避免同时运行两次;幂等性,则是让即使间隔很久,第二次执行也不会造成任何损害。夏令时造成的重复相隔一小时,所以防止重叠抓不住它。反过来,前一次执行时间变长而造成的重叠,仅靠幂等性是无法避免资源竞争的。两者都需要。

最后,必须改变监控的形态。不要看执行次数,而是把预期的区间列表与实际处理的区间列表进行比对。让缺失的区间和出现两次的区间各自可见,总数就不会再互相掩盖。

这种监控还有一个更大的价值:它连与时钟无关的事故也能一并抓到。节点挂掉导致跳过了一次,部署期间调度器停摆,前一次执行耗时过长导致下一次被跳过——原因各不相同,但症状都一样,即“那个区间没有被处理”。比对区间列表的监控不追问原因,只看这一个症状。比起为每种原因单独建立告警,它用少得多的规则覆盖了更多的事故。

再把整个课程浓缩成一句话,就是:带有时间的记录不是事实,而是写下它的那台机器的主张,判定期限的代码相信这个主张,并据此决定别人的命运。所以处理时间的代码必须分清两件事:哪些只是存储、哪些只是展示;以及哪些计算依赖“现在几点”,哪些计算依赖“过了多久”。只要守住这两个区分,本课程中见到的五个事件里,就有四个不会发生。

下一项实验要做什么

在本实验中,依次阅读:用错位的时钟打上时间戳的一批日志、只留下本地时间的发送记录、同时记录了挂钟时间和单调时钟的定时器记录、带有效区间的证书,以及定期结算的执行历史。亲手测量并写下错位的大小、重叠的区间和被跳过的执行,再亲手写三个能正确处理时间的函数。评分器会根据材料重新计算这些数字并加以核对,还会直接调用你写的函数来确认其性质。