cron 时区陷阱 - 避免定时任务的漂移
以本地时间配置的 cron 任务在夏令时切换期间会静默地重复执行或跳过。本文详解其故障模式,介绍以 UTC 运行调度的方案、Kubernetes CronJob 的 timeZone 字段,以及云调度器如何处理同样的问题。
夏令时转换每年两次打破时间连续性。在春季拨快时 (例如美国东部时间凌晨 2:00 跳至 3:00),2:00-2:59 的时间根本不存在 (间隙)。在秋季拨回时 (凌晨 2:00 退回到 1:00),1:00-1:59 的时间出现两次 (重叠)。这两种异常产生了数量惊人的软件 bug。
如果用户输入了一个处于间隙中的时间 (比如春季转换日的凌晨 2:30 EST),正确的行为并不明确。作为验证错误拒绝它?向前取整到 3:30 AM EDT?向后取整到 1:30 AM EST?每种选择都有道理。风险在于许多库默默地选择了一种,让期望另一种行为的开发者感到意外。
在夏令时转换日,一个日历日是 23 小时或 25 小时。“在当前时间上加 24 小时”和“给我明天的同一时间”通常一致,但在转换日它们不同。在春季拨快日凌晨 1 点加 24 小时得到次日凌晨 2 点,而“明天同一时间”是凌晨 1 点。
这种区别对循环调度 (“每天上午 9 点发送邮件”) 和截止时间 (“24 小时内取消”) 有实际影响。对于“每天相同的挂钟时间”,加一个日历日;对于精确的 24 小时物理时间,在 UTC 时间戳上加 86400 秒。设计应该明确区分这些意图,而不是将它们视为等同。
一个重复的“周二 14:00”约会应该在夏令时转换后仍然保持在当地时间 14:00。用户的意图是本地挂钟时间,而非固定的 UTC 时刻。因此重复事件应存储为 (时区, 本地时间) 对,每次出现时动态计算实际 UTC 时间。
RFC 5545 (iCalendar) 通过 DTSTART 和 RRULE 解决了这个问题。DTSTART 指定一个时区感知的本地时间,RRULE 描述重复模式。每次出现的 UTC 时间根据时区规则 (包括夏令时) 计算得出。采用这种设计,“本地 14:00”在转换后仍然是本地 14:00,避免了“我周一早上 9 点的定期会议在 3 月移到了 10 点”的尴尬投诉。
PostgreSQL 的 TIMESTAMP (WITHOUT TIME ZONE) 按原样存储值,不进行任何时区转换。TIMESTAMPTZ (WITH TIME ZONE) 在存储时将输入转换为 UTC,在读取时转换回会话的时区。要正确处理夏令时,TIMESTAMPTZ 是正确的默认选择。
如果你还需要记住用户表达的原始本地时间 (例如用户预约的时间),仅靠 TIMESTAMPTZ 会在转换时丢失该信息。模式是存储 TIMESTAMPTZ 加上原始时区名称在单独的列中,在显示时恢复用户的意图。这样既保留了绝对时刻,也保留了用户特定于区域的意图。
夏令时 bug 一年只出现两次,因此很容易在常规测试中漏掉。测试代码必须明确包含转换前后的时刻:输入间隙中的时间、解决重叠中的重复时间、跨转换日的时长计算,以及跨越转换的重复事件。没有这些特定的测试用例,bug 会在生产环境中暴露。
固定测试环境的时区也很重要。如果开发者在 UTC+9 (无夏令时) 上运行,但生产环境运行在 America/New_York (有夏令时),测试在本地通过但在生产中失败。要么将时区作为参数传递给被测函数,要么在测试运行期间显式设置 TZ 环境变量,以消除结果对环境的依赖。
这篇文章对您有帮助吗?
以本地时间配置的 cron 任务在夏令时切换期间会静默地重复执行或跳过。本文详解其故障模式,介绍以 UTC 运行调度的方案、Kubernetes CronJob 的 timeZone 字段,以及云调度器如何处理同样的问题。
了解夏令时的运作方式、哪些国家实行夏令时、转换发生的时间,以及夏令时如何使国际日程安排变得复杂。
夏令时在过去十年中不断失去政治支持。欧盟于 2019 年投票废除时钟调整,关于心脏病发作和昼夜节律紊乱的医学证据也在不断增加。本文综述了相关科学研究、节能神话以及各地区的实际决策。