为什么你的 crontab 总错 8 小时

cron 表达式不携带时区。0 9 * * * 并不绝对等于"早上 9 点"——它的意思是 "cron 守护进程所在时区的早上 9 点"。在 UTC 服务器上,这就是东八区的 17:00,比你想要的整整差 8 小时。

这是东八区开发者最常踩的 crontab 坑。你按本地时间写好表达式,部署到 cron 守护进程跑在 UTC 的服务器上, 结果夜间任务变成下午五点执行——更糟的是跨过午夜后落在了错误的日期。守护进程从不告诉你这件事正在发生。

UTC 与本地时间的错位:一个完整示例

假设你想在东八区(UTC+8)每个工作日早上 9 点跑一个报表任务,你写下:

0 9 * * 1-5

在 UTC 服务器上,这行在 UTC 09:00 触发——也就是上海时间 17:00。你的早报变成了下午报。 修法是把本地时间减去 8 小时得到 UTC 等价时间:

0 1 * * 1-5

现在服务器在 UTC 01:00 触发,对应上海时间 09:00。看上去很简单——直到涉及跨午夜的情况。

跨午夜的星期漂移

当小时偏移跨过午夜,星期也会跟着移动。假设你想在周一上海时间 02:00 执行一个任务:

0 2 * * 1

减去 8 小时后是 UTC 18:00,但那是前一天——也就是周日,而不是周一。正确的 UTC 表达式应该是:

0 18 * * 0

如果你只调整了小时,单纯转换后的表达式会悄悄继承错误的星期。这是一个沉默的 bug:任务仍然每周执行, 只是提前了一天,在 review 中极容易被忽略。

夏令时让问题更糟

只要任一时区使用夏令时,静态转换的表达式就只有半年是对的。美国东部在 EST(UTC-5)和 EDT(UTC-4) 之间切换时,你按冬季换算的任务在夏季会错一个小时——反之亦然。每年两个静默出错的窗口,每个持续数月。

稳妥的修法,在你的 cron 实现支持的前提下,是使用 CRON_TZ

CRON_TZ=America/New_York
0 9 * * 1-5  # 全年在纽约时间早上 9 点触发

CRON_TZ 把表达式钉到一个具名时区,让守护进程替你处理偏移和夏令时切换。 它被 Vixie cron、systemd timer(通过带时区的 OnCalendar) 以及大多数云调度服务所支持。如果你的实现不支持,就用 UTC 转换后的表达式,并在旁边加注释说明源时区—— 未来的你会感谢现在的你。

非整点偏移时区

印度标准时间是 UTC+5:30,尼泊尔是 UTC+5:45。如果你所在时区有非整点偏移,分钟字段必须吸收这个小数部分的差值。 IST 09:30 换算成 UTC 是 04:00——这里 30 分钟的偏移正好抵消,但并不总是如此。 45 分钟偏移的时区会产生作者几乎不会期望的分钟值。 在使用这类时区时,请务必仔细核对分钟字段。

正确地转换它

把你的表达式粘进 cron + 时区转换器,即可看到服务器端该写的那一行、 两个时区下的接下来几次执行,以及上面每个坑的警告——星期漂移、夏令时风险、非整点对齐问题—— 在上线之前全部一目了然。