跳到主要内容
AZ Tools

cron 定时任务究竟如何运行

一条 cron 表达式就是决定任务何时运行的五个小数字——而数量惊人的线上事故,正始于把它们读错。本文讲清语法、那条读起来与实际行为相反的规则、时区陷阱,以及让无人值守的调度保持可靠的习惯。

五个字段

标准 cron 依次接受五个字段:分(0–59)、时(0–23)、日(1–31)、月(1–12)、星期(0–6,0 为周日)。每个字段可以是单个值、列表(`1,15`)、区间(`9-17`)、步长(`*/15`),或表示「每个」的 `*`。因此 `0 9 * * 1-5` 表示周一至周五上午九点。

步长作用于它前面的区间,这正是分字段里的 `*/15` 表示「0、15、30、45 分」而不是「从任务创建时起每十五分钟」的原因。调度在当前时间与模式相符时触发:cron 不记录间隔,它盯着时钟。

「日」与「星期」是或,不是且

这是让所有人吃惊的规则。当日字段和星期字段「都」被限定时,cron 会在其中「任意一个」匹配时运行任务,而不是两者同时匹配时。因此 `0 0 13 * 5` 并不是「黑色星期五」:它每月 13 日会跑,每个周五也会跑。

该行为只在两个字段都被限定时出现。若其中一个是 `*`,另一个直接说了算。想要真正的交集,就得在任务内部加一道判断——在脚本开头检查日期,不符则提前退出——因为表达式本身无法表达它。

它跑在哪块表上

cron 表达式不带时区。调度按执行方所用的时区解释:经典 crond 用机器本地时区,多数托管调度器和 CI 系统用 UTC。于是同样的五个字段,因部署位置不同而对应不同的真实时刻——「午夜」报表落到下午就是这么来的。

如果执行方所在时区实行夏令时,每年会有两次让调度以两种方式出错。春季拨快的那一夜,落在被跳过那一小时内的任务根本不会运行;秋季拨慢的那一夜,重复的一小时可能让任务跑两遍。用 UTC 排期可以同时避开这两种情况,代价是当地墙上时间在一年中会漂移一小时——请有意识地选择你能接受的那种失败。

扩展语法、别名与第六个字段

不少调度器扩展了经典语法,而且互不兼容。Quartz 风格的表达式在最前面加了秒字段,所以六字段表达式与五字段完全是两回事,照搬来的调度可能以六十倍的频率触发。有些实现增加了 `L`(最后)、`W`(最近的工作日)和 `#`(当月第 n 个星期几);有些则在日期字段用 `?` 代替 `*`。

多数实现还接受别名:`@hourly`、`@daily`、`@weekly`、`@monthly`、`@yearly`,有时还有 `@reboot`。在合适的场合它们比数字更清晰,但会隐藏确切的分钟——`@daily` 是午夜,也正是这台机器上所有其他 `@daily` 任务一起启动的时刻。

设计能活下来的调度

假定某次运行会被漏掉、重复或彼此重叠,并让任务在这三种情况下都不会破坏数据。把工作做成幂等的,使重复执行无害;加锁,避免一次慢执行与下一次重叠;记录已处理的内容,让被漏掉的一次能够追平,而不是留下无声的缺口。

把调度错开。所有设成 `0 0 * * *` 的任务会同时启动,在共享主机上这种拥堵是自找的。选一个不整的分钟,让相关任务错峰,并给任何访问共享 API 的任务在开始时加一点随机延迟。最后,为任务「没有运行」这件事配告警:悄无声息停止触发的调度,看起来和无事可做的调度一模一样。

  • `*/15 * * * *` —— 每一刻钟,正好落在 0、15、30、45 分。
  • `0 9 * * 1-5` —— 工作日 09:00,按执行方所在时区。
  • `0 0 13 * 5` —— 每月 13 日「或」任意周五,而非「13 号的周五」。
  • `0 3 1 * *` —— 每月 1 日 03:00。

相关工具