同事贴了一条 cron: 0 9 star star 1-5,说每周一到周五早上 9 点跑。我看了一下,告诉他这周一会不跑。这不是开玩笑——5 个字段,每个都有自己的规则,搞错任何一个,schedule 就不是你想要的那个。

这篇文章就是为了让你下次看 cron 表达式的时候,不用打开 crontab(5) man 也能讲清楚它是怎么算的。

30 秒总览

  • Cron 表达式有 5 个字段,从左到右:minute hour day-of-month month day-of-week。
  • 5 个字段每个都可以是 star (任意)、数字、范围 (9-17)、步长 (star/15) 或列表 (1,15)。
  • Sunday 在 day-of-week 字段里是 0 (兼容 7),别写成 7,在有些调度器里会被拒绝。
  • day-of-month 和 day-of-week 两个字段都限制了的时候 (都不是 star),fire 规则是 UNIOR,任意一个匹配那天就触发。
  • DST 夏令时换班那一天,cron 可能错过一次或者跑两次,这是 cron 本身的限制而不是 bug。

5 个真实场景:5 个字段的常见误区

场景 1:minute 字段的 star/15 你真的理解吗

同事写了 star/15 star star star star,以为每 15 分钟跑一次。star/15 实际意思是:minute=0, 15, 30, 45。

陷阱:很多程序员以为 star/15 是从当前分钟开始算的下一分钟,然后每 15 分钟一次。错。它是从字段下界(0)开始,按步长 15 选值。所以 5:07:23 开始的调度,首次运行是 5:15:00,不是 5:22:23。

正解:用具体值或在 cron generator 里打开 star/15 看预览。比如 star/5 实际是 0, 5, 10, …, 55,5 个值,每 5 分钟间隔。

场景 2:hour 字段的范围表达式 9-17

老式教程告诉新人:工作时间是 9-17。0 9-17 star star star 实际意思是:9、10、11、12、13、14、15、16、17 这 9 个小时整点都跑。

陷阱:它不是 9 点工作到 17 点。17:30 不会跑(17 那次是 17:00:00),18 点开始也不会了。如果你要的是 9 点到 17 点之间每 30 分钟跑一次,得写 star/30 9-17 star star star——9 点、9:30、10 点、10:30 … 17 点、17:30,9 小时 x 2 = 18 次每天。

场景 3:day-of-month 的 star vs 具体数字 — 1 vs 2-29

任务:每月 1 号跑一次报告。0 0 1 star star 看着对,实际没事。

陷阱:有些团队想跑”月末最后一天”,写 0 0 L star star——但 L 是 Quartz 扩展,Vixie cron (Linux 默认的 cron) 不认。你的脚本要支持这个,得用 Quartz 或者当作 bug 改 schedule。

正解:可靠的月末方案是工作日迂回——如果月末是周末就跑最后一周五,0 0 star star 1-5 加 day-of-month 范围 28-31,然后在脚本里判断今天是不是工作日的”月末”。

场景 4:day-of-week 字段里 Sunday=0 还是 7

任务:每周日早上 3 点跑备份。同事写 0 3 star star SUN,你知道这是星期日;但他写 0 3 star star 7 你要怎么解释?

陷阱:Vixie cron / cronie 默认接受 day-of-week 字段的 0 和 7 都是 Sunday。但有些调度器 (Quartz 早期版本、部分 AWS EventBridge 早期配置) 只接受 0,会拒绝 7。

正解:永远写 0。0 是 RFC 标准,兼容性最好。把 7 当作 cron 解析器的扩展。

场景 5:month 字段简写 JAN FEB MAR … DEC

技术文档示例:0 9 star JAN-MAR star。语法上完全正确,等价于 0 9 star 1-3 star。

陷阱:简写只能用在 DAY-OF-WEEK 字段 (SUN MON TUE WED THU FRI SAT) 和 MONTH 字段 (JAN … DEC)。不能在 minute / hour / day-of-month 用简写。

正解:如果团队里有人用简写就能搞定,继续用;如果阅读里有歧义,改成 0-6 数字或 1-12 数字永远不出错。

4 个 cron 的反直觉事实

事实 1:day-of-month 和 day-of-week 是 OR 不是 AND

任务:每个周日或每月 1 号跑一次。0 0 1 star 0 看着像:每月 1 号 并且 是周日才跑——错。

正解:当 day-of-month 和 day-of-week 两个字段都不是 star 的时候,触发规则是 OR。任何匹配一天那天就 fire。所以 0 0 1 star 0 = 每月 1 号跑 + 每周日跑(可能会月度触发 1-2 次,看那个月日历)。

要想严格 AND,得用脚本判断或者换个调度器(k8s CronJob 支持更丰富的字段语义)。

事实 2:star 在不同字段含义不同

star 在 minute = 任意 0-59 整数,每分钟一次。star 在 hour = 任意 0-23 整数,每小时整点一次。差异在于频率——minute 的 star 是每分钟,其他字段的 star 都是该字段最大允许频率。

事实 3:步长 star/S 等价于下界/S

star/15 等于 0/15,只在 minute 字段,因为 minute 下界是 0。9-17/2 等于 9,11,13,15,17 这 5 个值。

陷阱:你写 1/15 (minute 字段) 实际是 1,16,31,46 这 4 个值,不是 从 1 开始每 15 分钟。

事实 4:DST 那一天 cron 可能错过一次或跑两次

北美夏令时切换(3 月第二个周日 / 11 月第一个周日)那一天,本地时间缺一小时或多一小时。cron 调度是基于本地 wall-clock 时间的,系统里实现各异。

正解:对时间敏感的任务不用 cron,用 systemd timer 或者 k8s CronJob,UTC 时区,跨 DST 不漂移。piick 上有 m3u8 player 教程写过 systemd timer 怎么配。

3 个真实调度场景的 cron 模板

场景 1:工作时间内每 15 分钟跑一次

star/15 9-17 star star 1-5

意思是:周一到周五,9 点到 17 点(包含 17),每 15 分钟一次。

频率:9 个小时 × 每小时 4 次 = 一天 36 次,周末不跑。如果你嫌吵改成 star/30 就 18 次/天。

场景 2:每周五下班前 18 点跑一次

0 18 star star 5

注意 day-of-week 字段值是 5,不是 FRI,但效果一样。建议两个都测试一下,你的部署 cron 接受哪种写法。

频率:一周一次,周五晚 6 点。下周一上班时报告应该已经发到邮件了。

场景 3:每月 1 号 0 点备份,如果 1 号是周末就提前到上一个周五跑

这用纯 cron 写不出来——得加一个包装脚本。

参考实现:写一个 bash 脚本(a.sh),脚本里先用 date 判断今天是不是月末。如果是月末,跑备份。然后 crontab 调 0 0 28-31 star star 1-5 /path/to/a.sh——每月最后一周工作日 0 点跑,a.sh 内部判断”是 1 号前一两天吗?是就备份。“

推荐做法

  1. 写 cron 别拍脑袋——打开 crontab.guru (https://crontab.guru) 或我们的 cron generator 工具,可视化你写的字段,看人类可读描述。
  2. 配 cron 前必须先看 next-run 列表——piick 的 cron-generator 默认 next 5 次运行,粘贴进去看是不是你期望的时间窗口。
  3. 时间敏感任务用 UTC + 显式 TZ——DST 是 cron 的原罪,用 systemd timer 或 k8s CronJob 配 spec.timezone=UTC 避开它。
  4. 别把”每月最后一天”塞 day-of-month 31 字段——靠 L 是 Quartz 扩展,跨平台不可靠;脚本内部判断更稳。
  5. 重要任务加 timeout 和 retry——cron 本身不关心任务是否跑完,kill -9 超时也得跑;重试用 systemd 的 OnFailure。

cron 是 Unix 文化里最实用的工具之一——5 个字段 + 简洁语法 + 跨平台稳定。我们做了 cron generator 工具,把字段变成下拉 + 实时预览 + next-run 列表,比手写不容易踩坑。

试试我们的 cron generator tool,切到 Visual Builder 或 Raw Editor,粘贴 star/15 9-17 star star 1-5 进去,看 next 5 runs 是不是周一到周五的 9-17 工作时间。如果是——恭喜你,你的第一份 cron schedule 成功落位。如果不是——用工具反馈的 next runs 找出哪个字段错了。