Cron 表达式解析器

解析和解释 Cron 计划表达式

minute (0-59) | hour (0-23) | day (1-31) | month (1-12) | weekday (0-6)

常用示例

DOM/DOW 的 OR 陷阱:为什么 '0 0 13 * 5' 不是 13 号的星期五

cron 中被误解最多的规则其实明明白白写在 POSIX crontab(5) 里:当 day-of-month 字段和 day-of-week 字段都被限制(即都不是 *)时,任意一个匹配就会触发任务。因此 0 0 13 * 5 会在每月 13 日的午夜和每个星期五的午夜都执行。你以为得到的是一年一到三次的"13 号星期五",实际上是每年约 60 次运行。由于两个触发条件各自看起来都合理,这个 bug 常常能通过 code review,直到有人问起"月度"报表为什么上周发了四次才暴露。 要获得 AND 语义,可靠的做法是只在表达式中限制其中一个字段,另一个放到命令里检查。把 day-of-month 固定为 13,让 shell 检查星期:date +%u 输出 ISO 星期(1 = 周一,7 = 周日),与 5 比较即可只选中周五。任务最多每年运行 12 次,不是周五的 13 号时守卫条件会静默退出。 这行命令里还藏着第二个陷阱:在 crontab 条目中,未转义的 % 是特殊字符。第一个 % 会截断命令——其后的内容被作为标准输入传给命令,后续的 % 则变成换行符。date +%Y-%m-%d 在交互式 shell 中运行得很好,在 cron 里却会悄悄截断你的任务。要么把每个 % 转义成 \%,要么把日期格式化挪进脚本,让 cron 只负责调用脚本。相当一部分"cron 跑了但什么都没发生"的工单,最后都归结到这一个字符上。
# OR semantics: fires every Friday AND every 13th (~60x/year)
0 0 13 * 5 /opt/report.sh

# AND semantics: pin DOM, test DOW in the command (max 12x/year)
0 0 13 * * [ "$(date +\%u)" = 5 ] && /opt/report.sh

# Unescaped % silently truncates the command line
0 1 * * * echo "backup-$(date +\%Y\%m\%d)" >> /var/log/runs.log

夏令时(DST):cron 任务的坟场

在会向前拨钟的时区里把任务排在 02:30,墙上时钟会从 02:00 直接跳到 03:00——任务应当执行的那个时刻当晚根本不存在。接下来发生什么因实现而异:vixie-cron 及其后继 cronie 能检测到跳变,在切换后不久补跑被跳过的任务;而容器里常见的极简 cron 克隆则直接跳过。秋季回拨更糟:01:00-02:00 这一小时会出现两次,幼稚的调度器会把任务触发两遍。如果这个任务负责开发票或扣款,你就每年给客户重复扣一次费。 有两种防御方式:把本地时间的调度放进安全窗口(03:00 及之后、02:00 之前),让时钟切换碰不到它;或者让调度器直接跑在没有任何切换的 UTC 上。 托管调度器还有自己的语义。GitHub Actions 的 schedule: cron 只按 UTC 解释,而且文档明确说明高负载时运行可能延迟数分钟甚至被丢弃——整点这类热门分钟受影响最大,所以选 17、43 这类冷门分钟。Kubernetes CronJob 在 1.27 中让 spec.timeZone 字段进入 stable。同时配好 startingDeadlineSeconds(允许开始时间最多晚多少——超过期限该次运行计为 missed)和 concurrencyPolicy:上一个任务还在运行时,下一个是与之共存(Allow,默认)、被阻止(Forbid),还是把旧的杀掉替换(Replace)。
# Kubernetes CronJob with explicit time zone (stable in 1.27)
apiVersion: batch/v1
kind: CronJob
spec:
  schedule: "30 4 * * *"
  timeZone: "Asia/Seoul"
  startingDeadlineSeconds: 300
  concurrencyPolicy: Forbid

*/N 不等于'每隔 N':步进值的真实语义

步进值 */N 展开为"该字段取值范围内、从范围起点开始每隔 N 的那些值"——而不是"每经过 N 个单位"。在分钟字段上两种理解恰好一致:*/15 得到 0,15,30,45,一小时正好 60 分钟,间隔均匀。到了 day-of-month 幻觉就破灭了:1-31/10 展开为 1、11、21、31 日——下个月又从 1 号重新开始,于是"31 号接着 1 号"变成连续两天各跑一次。 这意味着真正的"每 10 天"在 cron 里无法表达:日期计数器在每个月边界重置,而 cron 不记得上次运行是什么时候。同样的怪癖让 0 0 */2 * *("隔天执行")在 1,3,...,29,31 日运行后,下个月 1 日又运行——凡是有 31 天的月份都会出现连续两天执行,一年发生七次。 如果确实需要跨月边界的固定间隔,就让任务每天运行,在脚本里做门控:比较 epoch 天数对 N 取模,或把上次运行的时间戳存进文件、时间不够就提前退出。另一个选择是 systemd 定时器的 monotonic 调度(OnUnitActiveSec=10d),它从上次激活时刻起计数,完全不看日历。
*/15 * * * *      # minutes 0,15,30,45 — uniform, as expected
0 0 1-31/10 * *   # days 1,11,21,31 — NOT "every 10 days"
0 0 */2 * *       # 1,3,...,29,31 then the 1st: twice in a row

# real "every 10 days": run daily, gate on the epoch day
0 3 * * * [ $(( $(date +\%s) / 86400 \% 10 )) -eq 0 ] && /opt/task.sh

一种语法,多种方言:Quartz、EventBridge 与 systemd 定时器

5 字段的 POSIX 形式(分、时、日、月、星期)只是基准线。嵌入无数 Java 服务的调度器 Quartz(包括 Spring 的 @Scheduled)使用 6 或 7 个字段:秒排第一,可选的年份排最后,并且拒绝 DOM 和 DOW 同时为具体值的表达式——其中一个必须是表示"无特定值"的 ?。作为交换,你得到真正有用的运算符:L 表示当月最后一天(6L 是最后一个周五),W 表示离指定日期最近的工作日(15W),# 表示第 n 个星期几,MON#2 就是第二个周一——这些是纯 cron 根本无法表达的。 AWS EventBridge 有自己的 6 字段方言,以年份字段(1970-2199)结尾,并且像 Quartz 一样拒绝 DOM 和 DOW 同时为 *。把 crontab 行直接复制进 EventBridge 规则,常常要把其中一个字段换成 ? 才能通过校验。 经典 cron 还接受 @reboot、@daily、@hourly 这类昵称——方便,但非标准且支持参差不齐,需要可移植性的地方要避开。如果需要秒级精度或补跑(catch-up)语义,systemd 定时器是现代答案:OnCalendar 接受秒(Mon..Fri 09:00:30),Persistent=true 会记录上次触发时间,在下次开机时补跑错过的任务——这正是 cron 出了名缺失的行为,也是为睡过调度时刻的笔记本准备的 anacron 存在的原因。
0 9 * * 1-5            # POSIX: weekdays at 09:00
0 0 9 ? * MON-FRI      # Quartz: seconds first, ? required in DOM
0 0 9 ? * MON#2        # Quartz: second Monday of the month
0 9 ? * MON-FRI *      # EventBridge: year field appended

# systemd timer unit equivalent
OnCalendar=Mon..Fri 09:00:00
Persistent=true        # run missed jobs after downtime

运维卫生:重叠执行、丢失的输出与悄然停摆的任务

cron 没有补跑机制:任务该运行的时刻如果主机宕机或守护进程停着,那次运行就没了——没有队列,也没有重试。对错过一次就出问题的任务(日志轮转、证书续期、账务),要么迁到带 Persistent=true 的 systemd 定时器,要么让任务自身的逻辑能容忍空档。 下一个意外是输出。cron 会把任务写到 stdout 或 stderr 的所有内容,通过本地 MTA 邮寄给 crontab 属主;在没配 MTA 的现代机器上,这些输出直接蒸发。想要真正的告警就设置 MAILTO,否则用 >> /var/log/job.log 2>&1 显式重定向——并记得轮转那个文件,cron 会乐此不疲地往里追加好几年。 慢任务与下一次调用重叠是经典的数据损坏来源——两个 rsync 写同一个目录,两个迁移跑同一个数据库。用 flock -n /path/lock 包住命令,第二个实例会立即退出而不是越积越多。还要当心惊群效应(thundering herd):全世界数量惊人的 crontab 都写着 0 0 * * *,共享数据库和 API 在午夜整点被集中轰炸。cronie 支持 RANDOM_DELAY,systemd 有 RandomizedDelaySec,也可以用主机名哈希推导出分钟,让整个集群自动散开。 最后,最危险的故障模式是任务悄无声息地停跑了——一次错误的编辑、被禁用的账号、写满的磁盘。cron 不会告诉你。把监控反过来,用 dead-man's-switch:任务每次成功就 ping 一个 URL(healthchecks.io 模式),告警在 ping 停止到达时触发,而不是在某处报错时。
[email protected]
RANDOM_DELAY=15    # cronie: spread the start by 0-15 minutes

# no overlap, logged output, dead-man's-switch ping
17 3 * * * flock -n /run/backup.lock /opt/backup.sh \
  >> /var/log/backup.log 2>&1 \
  && curl -fsS https://hc-ping.com/UUID > /dev/null
最后更新:

关于此工具

Cron 表达式解析器把 Unix cron、GitHub Actions、Kubernetes CronJob 等使用的 5 字段语法翻译成自然语言,并显示下几次执行时间。5 个字段为分钟/小时/日/月/星期,支持通配符(*)、范围、列表、步进等语法。

使用方法

  1. Type a cron expression like 0 9 * * 1-5 into the input.
  2. Read the human-readable description that appears below — it confirms what your expression actually means.
  3. Inspect the next execution times to verify edge cases (month boundaries, leap years, etc.).
  4. Click any example expression to load it as a starting point.
  5. Copy the validated expression into your crontab, GitHub Actions workflow, or Kubernetes manifest.

常见用例

  • Scheduling a daily database backup at 3 AM (0 3 * * *).
  • Running a CI workflow every Monday morning (0 9 * * 1).
  • Triggering a cleanup job every 15 minutes (*/15 * * * *).
  • Sending a weekly digest email Friday afternoon (0 17 * * 5).
  • Defining a Kubernetes CronJob to rotate logs at midnight on the first of each month.
  • Validating a tricky expression that uses ranges and steps before deploying it.

常见问题

Q. Why does my expression run at unexpected times?

A. Cron interprets day-of-month and day-of-week as OR (not AND) when both are restricted. * * 15 * 1 fires on the 15th OR every Monday — not just Mondays falling on the 15th.

Q. What time zone does cron use?

A. Whatever the host or container is configured for. GitHub Actions cron runs in UTC. Kubernetes CronJobs default to the cluster controller manager time zone unless you set spec.timeZone.

Q. Are 6-field cron expressions supported?

A. Some schedulers (Quartz, AWS EventBridge) add a seconds or year field. This parser uses the standard 5-field POSIX format.

Q. How do I run a job once at a specific time?

A. Cron itself does not have a one-shot mode. Use systemd timers, an at job, or an event-based trigger instead.