cron-doctor
概述
Cron 看似简单,实则极易出错。其失败模式是静默的 —— 一个语法合法的表达式,实际上从未触发,或者触发的频率远超预期。0 0 30 2 * 解析没问题,但永远空转(2 月没有 30 日)。0 0 1,15 * 1 看起来像"如果是周一则在 1 号和 15 号触发",但实际含义是"1 号、15 号,或每个周一" —— 每月约触发 6 次,而非约 2 次。
本技能教会智能体在上线前捕获这些问题。它自带一个零依赖的验证引擎(scripts/cron-engine.js,无需安装),用于解析、描述、深度验证并计算下次触发时间。
何时使用本技能
- 当用户编写、编辑、审查或部署 cron 表达式时 —— 无论是在 crontab、Kubernetes
CronJob、GitHub Actionsschedule、Airflow DAG、Celery beat 调度、systemd 定时器,还是其他任何定时任务中。 - 当调试一个"没触发"或"在错误的时间触发"的作业时。
- 当用户问"这个 cron 表达式是什么意思?""下次什么时候跑?""一年跑多少次?"时。
- 当审查包含
schedule字段的 CI/CD 流水线或基础设施配置时。 - 当用户粘贴一个 5 字段的 cron 表达式并请求健康检查时。
工作原理
步骤 1:解析表达式
按空白拆分为 5 个字段:分钟、小时、日期、月份、星期。确认取值范围合法:
| 字段 | 位置 | 范围 | 备注 |
|---|---|---|---|
| minute(分钟) | 1 | 0–59 | |
| hour(小时) | 2 | 0–23 | |
| day-of-month(日期) | 3 | 1–31 | |
| month(月份) | 4 | 1–12 | 接受名称(JAN–DEC) |
| day-of-week(星期) | 5 | 0–7 | 0 和 7 都代表周日;接受名称(SUN–SAT) |
步骤 2:用通俗语言描述它
说明用户以为它做什么,与它实际做什么。对于日期 + 星期字段,要明确指出 OR 与 AND 语义(见死亡陷阱 #2)。
步骤 3:运行陷阱检查清单
检查下面的五个死亡陷阱,并标记出命中的项。
步骤 4:计算下次触发时间与年度触发次数
具体计算出接下来 5 次触发时间作为具体日期,便于用户验证调度行为是否符合预期。估算年度触发次数 —— 一个一年触发 365 次的计划与一年触发 12 次的计划,在成本与负载上相差约 30 倍。
五种 Cron 死亡陷阱
这些都是能通过 crontab -l 验证、却在生产环境翻车的 bug。
1. 不可达日期 —— "永不触发"的 bug
0 0 30 2 *
语法合法。永不触发。 2 月没有 30 日。这条调度是一个沉默空转的死作业。在任何只有 30 天的月份里指定 31 日也同样如此:0 0 31 4 *、0 0 31 6 *、0 0 31 9 *、0 0 31 11 *。
修复方法: 使用 0 0 28-31 * * 并在脚本里判断月末,或在调度器支持时使用 L(最后一天)语法。
2. OR 语义 —— "触发太频繁"的 bug
0 0 1,15 * 1
并不意味着 "如果是周一,则在 1 号和 15 号的零点触发"。 实际含义是 "1 号、15 号,或每个周一的零点触发"。每月约 6 次,而非约 2 次。
这是 cron 中被误解最深的一条规则。当日期和星期****都被限制(都不是 *)时,cron 使用 OR 逻辑,而非 AND。
修复方法: 如果你需要"仅当周一时在 1 号和 15 号触发",改为每天运行并在脚本内判断:
0 0 * * 1 [ "$(date +%d)" = "01" -o "$(date +%d)" = "15" ] && your-command
3. 午夜流量尖峰 —— "万事齐发"的 bug
0 0 * * *
所有调度在 0 0 的作业会同时争抢资源。数据库备份、日志轮转、证书续期、报表生成 —— 全部同时触发。这会导致负载尖峰、连接池耗尽以及级联超时。
修复方法: 把作业分散到不同时段。改用 17 2 * * * 或 43 3 * * * 代替 0 0。错峰(jitter)是你的好朋友。
4. 不均匀步长 —— "漂移"的 bug
*/7 * * * *
并不意味着 "每 7 分钟均匀执行"。它的实际含义是"每 7 分钟执行一次,从 0 开始,60 时归零"。所以触发序列为:0、7、14、21、28、35、42、49、56 —— 然后回到 0(间隔 4 分钟)。间隔序列漂移为:7,7,7,7,7,7,7,7,4。
修复方法: 60 不能被 7 整除。使用能整除 60 的步长:*/5、*/10、*/15、*/20、*/30。如果确实需要每 7 分钟一次,使用带 sleep 420 的循环。
5. 闰年 2 月 29 日 —— "年度惊喜"
0 0 29 2 *
仅在闰年触发 —— 2024 / 2028 / 2032 年的 2 月 29 日…… 如果有人期望这是"2 月底",那么 4 年中会有 3 年感到困惑。
修复方法: 使用 0 0 28 2 *,如需要 29 号的情况在脚本内处理。
使用验证脚本
本技能附带一个零依赖引擎,位于 scripts/cron-engine.js(Node.js,无需 npm install)。你可以以编程方式或从 CLI 使用它:
// 编程式 —— Node.js,零依赖
const { describe, validate, nextRuns, formatNextRuns } = require('./scripts/cron-engine.js');
// 解析 + 描述 -> 返回 { text, error, parsed }
const d = describe('0 0 30 2 *');
console.log(d.text); // "At 00:00, on day-of-month 30 in in FEB"
// 深度验证 -> 捕获各种陷阱
const result = validate('0 0 30 2 *');
console.log(result.valid); // true(语法合法)
console.log(result.observations); // 包含"永不触发"洞察
console.log(result.suggestions); // 如 "Midnight is a common spike..."
// 下 5 次触发时间 -> 返回 Date[]
const runs = nextRuns('0 9 * * 1-5', new Date(), 5);
console.log(formatNextRuns(runs, new Date())); // [{ date, relative, formatted }, ...]
# CLI(通过内置包装器)
node scripts/cli.js describe "*/5 * * * *"
node scripts/cli.js validate "0 0 30 2 *"
node scripts/cli.js next "0 9 * * 1-5" 5
常用 cron 预设
| 表达式 | 描述 | 用途 |
|---|---|---|
*/5 * * * * |
每 5 分钟 | 健康检查、轮询 |
0 * * * * |
每小时 | 按小时聚合 |
0 */2 * * * |
每 2 小时 | 中频同步 |
0 9 * * 1-5 |
周一至周五 9 点 | 工作时段任务 |
0 2 * * * |
每天凌晨 2 点 | 非高峰批处理(避开午夜) |
0 0 * * 0 |
周日零点 | 每周维护 |
0 0 1 * * |
每月 1 号零点 | 月度报表 |
0 0 1 1 * |
1 月 1 日零点 | 年度任务 |
最佳实践
- ✅ 始终给出通俗语言描述 AND 运行陷阱检查清单。
- ✅ 将午夜作业错峰以避免流量尖峰。
- ✅ 优先选择能整除 60 的步长(
*/5、*/15、*/30)。 - ✅ 在每条 crontab 上方添加注释解释意图。
- ✅ 在支持的调度器上设置显式时区(
CRON_TZ)。 - ❌ 不要迷信
crontab -l验证 —— 它只检查语法,不检查语义。 - ❌ 不要在未确认 OR 逻辑的情况下同时限制日期与星期。
- ❌ 不要把所有作业都排在
0 0。
常见陷阱
问题: "我的 cron 作业没有运行。" 解决方案: 检查是否有不可达日期(陷阱 #1),并确认守护进程正在运行(
service cron status/systemctl status crond)。确认文件以换行符结尾,并具有正确的属主。问题: "我的作业触发次数远超预期。" 解决方案: 你遇到了 OR 语义(陷阱 #2)。如果日期与星期都被设置,cron 会将它们按 OR 处理。将其中一个改为
*,或在脚本内加判断。问题: "间隔不均匀 —— 有时 7 分钟,有时 4 分钟。" 解决方案: 步长值不能整除 60(陷阱 #4)。使用 60 的因数。
问题: "我的作业在本地能跑,但在集群里不行。" 解决方案: 时区不匹配。Kubernetes
CronJob和 GitHub Actions 默认使用 UTC。确认timeZone/TZ已按预期设置。
局限性
- 本技能针对标准的 5 字段 cron,涵盖 Vixie cron、systemd timer、Kubernetes
CronJob、GitHub Actionsschedule以及大多数类库的实现。它不验证 Quartz 的 6/7 字段(带秒/年份)表达式,也不验证非标准的@reboot/L/#扩展,除非另有说明。 - 年度触发次数估算以非闰年作为参考;2 月 29 日的调度(陷阱 #5)会被显式标记。
- 本技能不能取代环境特定的验证、测试或专家审查。如果缺少必要的输入、权限或安全边界,请停下来澄清。
相关技能
docker-expert—— 当 cron 作业运行在容器内,问题出在容器/入口点而非调度时。kubernetes-deployment—— 当验证CronJob清单的spec.schedule字段与其他资源配置时。
安全与安全说明
本技能是只读的,risk: safe。验证脚本不执行任何文件写入、网络调用或变更 —— 仅解析与计算。可以无条件安全地对任何 cron 表达式运行。