标准 Cron 表达式有五个字段:分钟、小时、月内日期、月份和星期。每个字段支持数字、范围、列表、步长和通配符。从左到右阅读,注意日期和星期是“或”的关系,并在部署前确认目标时区。
- 五个字段依次是分钟、小时、月内日期、月份、星期。
- */5 这类步长只作用于所在字段,不代表整个调度每隔 5 分钟。
- 经典 Cron 中日期和星期同时受限时是“或”关系,很少符合预期。
- Cron 使用服务器本地时区,夏令时切换可能跳过或重复任务。
- 上线前先验证表达式,并确认接下来几次运行时间。
Cron 表达式很短,却很容易读错。一个字符就能改变调度,同样的表达式在不同实现里也可能表现不同。本文讲清楚实践中真正重要的语法,以及在上线前发现错误的检查方法。
五个字段
标准 crontab 一行由五个时间字段和命令组成:
* * * * * command
│ │ │ │ │
│ │ │ │ └── 星期(0-7,星期日是 0 或 7)
│ │ │ └──── 月份(1-12)
│ │ └────── 月内日期(1-31)
│ └──────── 小时(0-23)
└────────── 分钟(0-59)
字段是位置相关的。0 9 * * 1 表示每周一 09:00,而不是每月 1 日 09:00。
每个字段的范围很容易记混:分钟 0–59,小时 0–23,月内日期 1–31,月份 1–12,星期 0–7,其中 0 和 7 都表示星期日。超范围的值通常会在安装时被拒绝,但错误可能只写进系统日志,于是 crontab 看起来装好了,那一行却被悄悄忽略。
crontab 还接受五个字段之外的写法。@daily、@hourly、@weekly、@monthly 和 @reboot 被广泛支持,但它们不属于 POSIX 规范,精简的 cron 实现可能直接拒绝。以 # 开头的行是注释。命令里的 % 有特殊含义:cron 把它当作换行,后面的内容会变成标准输入,所以 date +%F 这类命令可能把自己截断。应写成 %,或者把命令移进脚本。
字段语法
每个字段支持:
| 语法 | 含义 | 示例 |
|---|---|---|
| * | 所有值 | 分钟字段的 * 表示每分钟 |
| 5 | 单个值 | 第 5 分钟 |
| 1-5 | 范围 | 1 点到 5 点 |
| 1,15,30 | 列表 | 第 1、15、30 分钟 |
| */15 | 从字段起点开始步进 | 每 15 分钟 |
| 10-50/10 | 在范围内步进 | 第 10、20、30、40、50 分钟 |
步长在每个单位起点重新开始。分钟字段的 */15 会在 0、15、30、45 分运行,而不是“距离上次运行 15 分钟”。
Vixie 系 crontab 还支持月份和星期的三字母缩写,例如 JAN、MON、FRI,但并非所有实现都认。范围和列表可以混用名称与数字,读起来却比纯数字更费劲。应选定一种风格并保持一致。带显式起点的步长,例如 5/15,在不同实现里含义不同:有的读作“从第 5 分钟开始,每 15 分钟”,有的直接报语法错误。起点重要时应写 5-59/15。
10-50/10 也值得仔细读:它表示 10、20、30、40、50,而不是从字段起点开始每隔十个值。如果范围不能被步长整除,末尾超出的值不会被调度,单个字段内部也没有回绕。
常见调度写法
- 每分钟:* * * * *
- 每 5 分钟:*/5 * * * *
- 每小时整点:0 * * * *
- 每天 03:30:30 3 * * *
- 工作日 09:00:0 9 * * 1-5
- 每月 1 日零点:0 0 1 * *
- 每周日 23:00:0 23 * * 0
- 工作时间每 15 分钟:*/15 9-17 * * 1-5
这些示例都假设服务器时区符合预期,而这个假设值得单独检查。
有些需求无法用五个字段干净表达。“每月最后一天”不是 Cron 字段,31 号在小月不会运行。“每月最后一个工作日”“第三个星期四”“每 90 分钟”都超出了五字段语法。可行的绕法是每天运行并在命令里判断日期,或换用支持日历和间隔语义的调度器。用看似聪明的范围去近似,通常会得到一条每年错一次的表达式,而那次往往没人盯着。
日期与星期的经典陷阱
在经典 Cron 中,如果月内日期和星期都受限,任务会在任意一个条件满足时运行。这个表达式:
0 0 13 * 5
并不是“黑色星期五”,而是“每月 13 号或每个星期五”,一年大约运行 60 次,而不是一两次。
实用规则:
- 只限制一个日期字段。
- 只有在 Quartz 等支持 ? 的调度器中才使用 ?。
- 需要同时满足两个条件时,可以每天运行并在命令里判断日期,或改用支持日历逻辑的调度器。
算一下就清楚了。一年有 12 个 13 号,加上大约 52 个星期五,共 64 个候选日;减去一两个恰好重合的月份,实际约 62 次。如果本意是“13 号且星期五”,这条表达式错了大约 30 倍。
这不是某个实现的 bug。POSIX 规定两个日期字段都受限时按“或”执行,Vixie cron 及其后代都照此实现。Quartz 反过来要求其中一个字段写 ?,所以从 Linux 复制过去的表达式可能直接被拒绝。两个字段都写 * 时每天都会匹配,或逻辑反而无害。
时区与夏令时
Cron 默认使用服务器本地时区,除非实现明确支持时区设置。这会带来两个问题:
- 不同区域的服务器会在不同的绝对时间运行同一份 crontab。
- 夏令时切换可能跳过或重复某个小时。
需要稳定时间的调度应优先使用 UTC。必须跟随本地时区的业务任务,应在调度器允许的地方显式设置时区,并测试切换日期。安排在本地时间 02:30 的任务,在时钟向前拨的那天可能不会运行。
看一个具体例子。美国东部时间 2026-03-08 凌晨 02:00 拨快到 03:00,02:00 到 02:59 这一小时不存在。America/New_York 时区里写 30 2 * * * 的任务,当天找不到对应的时刻:有的实现直接跳过,有的在 03:00 补跑,和原本 03:00 的任务撞在一起。到了 2026-11-01,01:00 到 01:59 重复一次,30 1 * * * 可能运行两遍,墙钟标签相同,对应的 UTC 时刻却不同。欧洲的切换日期是 2026-03-29 和 2026-10-25,被跳过的小时同样是 02:00 到 02:59。如果任务负责扣款、发提醒或轮换凭证,跑两遍通常比晚跑一遍更糟。
不同实现的行为不一样,所以测试很重要。Vixie cron 及其后代都会感知时钟变化,并有显式的 DST 处理,但具体结果随版本和发行版而变:被跳过区间里的任务可能在跳变后补跑一次,重复区间里的任务可能跑两遍。Debian cron、cronie 和 BusyBox cron 在这里并不等价。systemd timer 用日历语义计算下一次触发,并明确记录 DST 行为,但结果仍取决于 OnCalendar 表达式。较新的 Kubernetes CronJob 支持 timeZone,错过的运行则由 startingDeadlineSeconds 决定。稳妥的模式是:任务含义是绝对时间就用 UTC 调度;必须跟随本地营业时间时,让任务本身可幂等重试。
Cron 不是间隔计时器
Cron 描述的是墙上时钟时间,不是经过的时间间隔。一个运行 40 分钟的任务如果每 30 分钟调度一次,就会重叠;安排在零点的任务也不会在夏令时切换后保持相同间隔。
如果需要“每 90 分钟”或“上次完成后 20 分钟”,Cron 不是合适的工具。应使用队列、工作循环,或支持间隔与并发控制的调度器。如果必须使用 Cron,应加锁,避免两次运行重叠。
重叠很容易想象。00:00 启动的任务要跑 40 分钟,00:30 Cron 又启动一次,此时第一次还没结束。两个进程写同一个输出文件、给同一个计数器加一,第二次还可能读到写了一半的状态。用 flock -n 加锁能让第二次立即退出,这通常比排队更好,但被跳过的运行必须可以接受。如果不能接受,说明用错了层次,应该换成队列。
systemd timer 提供折中方案:OnUnitActiveSec=90min 按上次激活时间计算,而不是按墙钟槽位;service 单元还可以限制同时只跑一个实例。在现代 Linux 上,这往往是不引入队列时最干净的修法,而且 systemctl list-timers 可以直接用来验证。
环境与路径差异
crontab 运行时的环境非常精简,常见故障包括:
- PATH 比交互式 shell 短,导致 command not found。
- 相对路径从错误的目录解析。
- .bashrc 或 .zshrc 里的环境变量没有被加载。
- 没有配置邮件,输出直接消失。
应使用绝对路径,在 crontab 或包装脚本中显式设置需要的变量,并把输出重定向到日志文件。
默认环境比多数人想象的更精简。PATH 通常是 /usr/bin:/bin,SHELL 是 /bin/sh,HOME 可能有值,而 LANG、NODE_ENV 和应用密钥都没有。交互式 shell 里能跑的命令,在 Cron 里可能因为装在 /usr/local/bin、缺少版本管理器 shim,或者工作目录是用户主目录而不是项目目录而失败。Cron 也不会读取 .bashrc 或 .zshrc,那些文件只服务于交互式 shell。
输出处理是任务的一部分。配置了邮件系统时,标准输出和标准错误会寄给用户;没有配置时,它们会被直接丢弃。应把两者重定向到带时间戳的日志文件,或者把命令包进脚本,写入 syslog 或结构化日志。否则故障的第一个信号会是监控面板数据不再更新,那已经太晚了。
部署前验证
- 把表达式翻译成一句可读的调度描述。
- 在目标时区下检查接下来十次运行时间。
- 确认调度器方言:5 字段还是 6 字段。
- 检查日期字段的行为。
- 用相同的用户和环境手动执行命令。
- 增加日志和失败告警。
Cron 表达式生成器可以显示接下来的运行时间,并帮助把表达式翻译成自然语言。调度跨时区时,可以用 Unix 时间戳转换工具把时间换算成绝对时刻再比较。
两种验证手段针对不同错误。生成器确认表达式的含义,但不知道服务器时区。systemd-analyze calendar '--* 02:30:00' 会打印 systemd 表达式的后续触发时间,并且显式处理 DST,应在切换日期前后各跑一次。经典 crontab 则可以把时间临时改到下一分钟,观察日志后再改回去。只有让真实调度器跑一次,才能发现环境和权限问题。
Cron 的不同实现
并非所有 Cron 都是经典 Cron:
- Vixie cron 和多数 Linux crontab 使用五个字段,并采用前面说的“或”行为。
- Quartz 使用六个或七个字段,包含秒,并支持 ?、L 和 W。
- Kubernetes CronJob 使用五个字段,较新版本提供独立的 timeZone 字段。
- 云调度服务 通常还提供时区、重试和补跑选项。
在不同系统之间直接复制表达式,很容易得到一个运行过于频繁或永远不运行的调度。
部署流水线里还有两种常见方言。GitHub Actions 使用五字段 POSIX 语法,但始终按 UTC 计算,最短间隔是五分钟;从本地时区服务器复制来的表达式会整体偏移一个时差。AWS EventBridge 使用带年份的六字段格式,要求月内日期或星期之一写 ?,并按调度配置的时区执行。Jenkins 的语法又不同,支持 H 散列值和独立的 @daily 形式。表达式看起来眼熟,背后的解析器却未必是你测试过的那一个。
部署检查清单
- 字段数量符合目标调度器。
- 只限制了一个日期字段。
- 时区明确且正确。
- 接下来的运行时间符合需求。
- 命令使用绝对路径。
- 输出写入日志。
- 已防止运行重叠。
- 失败会触发告警。
- 检查过 DST 切换日期,而不只是今天的下一次运行。
- 任务可幂等重试,或重复执行无害。
Cron 表达式是与时钟签订的契约。逐字段阅读,确认方言和时区,并在上线前验证接下来的运行时间。
遇到这些情况就不该用 Cron:任务必须严格每隔 N 分钟运行;单次运行经常超过间隔;触发条件取决于事件而不是时刻;宕机后错过的运行必须补上。此时应使用带可见性超时的队列、支持持久定时器的工作流引擎,或明确提供补跑语义的 systemd timer。Cron 只擅长一件事:在某个墙钟时刻启动一条短小、独立的命令。把它用在这件事上,其余需求交给能表达该需求的调度器。
常见问题
*/5 * * * * 是什么意思?
每 5 分钟运行一次。分钟字段是 */5,其余字段为通配符。它会在服务器本地时区的 0、5、10 等分钟触发。
为什么 0 0 13 * 5 比预期运行得更频繁?
经典 Cron 把月内日期和星期按“或”处理。这个表达式会在每月 13 号或每个星期五运行,而不是只在既是星期五又是 13 号时运行。需要同时满足两个条件时,应只保留一个日期字段,在命令里判断日期,或改用其他调度器。
如何每 90 分钟运行一次?
单条 Cron 无法干净地表达 90 分钟间隔,因为分钟和小时字段会在 24 小时周期内重复。可以用两条错开的表达式、带循环的包装脚本,或支持间隔语法的调度器。
Cron 会处理夏令时吗?
Cron 跟随系统时钟。时钟向前拨时,落在被跳过小时内的任务可能不运行;时钟向后拨时,任务可能运行两次。关键任务应使用 UTC,或使用明确支持时区和夏令时的调度器。
5 字段和 6 字段 Cron 有什么区别?
5 字段 Cron 从分钟到星期。6 字段变体(例如 Quartz)通常在开头增加秒字段,有的还会增加年份字段。复制表达式前,先确认目标调度器的格式。