用本工具把 cron 表达式拼出来、确认下次执行时间符合预期、贴到服务器 crontab 里——本以为大功告成,结果定时任务一次也没跑。
这种”表达式正确但任务不触发”是 cron 最让人崩溃的场景。本文不再讲表达式语法(已有 Cron 5 分钟入门 和 时区 / DST 陷阱),专攻”配置上去不跑”的 9 类原因和排查路径。
第一步:看日志确认 cron 有没有起任务
90% 的”不跑”问题,在日志里就有答案。
| 系统 | cron 系统日志路径 |
|---|---|
| Debian / Ubuntu | /var/log/syslog(过滤 grep CRON) |
| CentOS / RHEL / Rocky | /var/log/cron |
| macOS | log show --predicate 'process == "cron"' --last 1h |
| 容器(Docker) | 通常无日志——cron 服务可能没启 |
# Debian
grep CRON /var/log/syslog | tail -20
# CentOS
tail -20 /var/log/cron
两种典型输出:
Oct 10 08:15:01 host CRON[12345]: (me) CMD (python /home/me/job.py)
→ cron 起了任务(CMD 行),如果脚本没生效问题在脚本本身(继续看下面)。
(没有任何 CRON 日志)
→ cron 根本没起任务,问题在 cron 服务 / 表达式 / 用户身份(继续看下面)。
第二步:cron 没起任务的 4 种原因
A. cron 服务没在跑
systemctl status cron # Debian/Ubuntu
systemctl status crond # CentOS
Active: active (running) 才行。否则:
systemctl start cron && systemctl enable cron
Docker 容器特别注意——基础镜像默认不启 cron 服务,需要:
CMD ["cron", "-f"] # 前台运行
# 或
CMD ["sh", "-c", "cron && tail -f /var/log/cron.log"]
B. 用户 crontab vs 系统 crontab 弄反
| 位置 | 编辑方式 | 用户身份 |
|---|---|---|
| 用户 crontab | crontab -e | 当前用户 |
系统 crontab /etc/crontab | 直接编辑文件 | 多一个”用户”字段 |
/etc/cron.d/myjob | 直接编辑文件 | 多一个”用户”字段 |
/etc/cron.daily/myjob | 放可执行脚本 | root |
最常见错乱:把”用户 crontab”格式(5 字段 + 命令)写进了 /etc/cron.d/(应 5 字段 + 用户 + 命令):
# /etc/cron.d/myjob 正确格式:
15 8 * * * me /home/me/job.sh
↑↑
用户字段
漏了用户字段 → 整行不解析。
C. crontab 文件结尾没换行
POSIX 规定 crontab 必须以换行结尾。漏了最后一个 newline——最后一个任务被吞,cron 不解析。
修复:
echo "" >> /etc/cron.d/myjob # 加一行空行
或者用 crontab -e 编辑时编辑器(vim、nano)会自动加结尾换行。
D. 表达式的”日 OR 周”陷阱
cron 表达式有个反直觉规则:
* * * * *
分 时 日 月 周
**当日字段和周字段都不是 ***时,触发条件是”日匹配 OR 周匹配”,不是 AND。
15 8 1,15 * 1-5
→ 不是”工作日的 1 号和 15 号”,而是”每月 1/15 日 OR 每个工作日”——几乎天天跑。
修复:
| 想要的语义 | 写法 |
|---|---|
| 每月 1/15 日 | 15 8 1,15 * *(周字段 *) |
| 每个工作日 | 15 8 * * 1-5(日字段 *) |
| 工作日 + 月初月中(AND) | 表达式做不到,脚本里加 if [ $(date +%d) -eq 1 ] || [ $(date +%d) -eq 15 ] |
第三步:cron 起了任务但跑不通的 5 种原因
E. PATH 太短,命令找不到
cron 默认 PATH:
/usr/bin:/bin
你登录 shell 的 PATH:
/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:...
差异巨大——所有装在 /usr/local/bin、/opt/xxx/bin、~/.nvm/...、~/.cargo/bin 下的命令在 cron 里都找不到。
修复(推荐顺序):
# 1. 命令用绝对路径(最稳)
* * * * * /usr/local/bin/python3 /home/me/job.py
# 2. crontab 顶部设 PATH
PATH=/usr/local/bin:/usr/bin:/bin:/home/me/bin
* * * * * python3 /home/me/job.py
# 3. 脚本里 source 完整环境
* * * * * bash -lc '/home/me/job.sh'
# -l 让 bash 走登录 shell 流程,加载 /etc/profile / ~/.bash_profile
F. 命令里有 % 没转义
cron 把 % 当 stdin 分隔符——第一个 % 之后的内容被当成命令的输入。
# 错误(被截断在第一个 %):
* * * * * echo "$(date +%Y-%m-%d)" >> /tmp/log
# 正确(每个 % 转义):
* * * * * echo "$(date +\%Y-\%m-\%d)" >> /tmp/log
全文搜替换:crontab 里所有 % 一律改 \%,除非你故意当 stdin。
G. 输出没重定向,磁盘 / 邮件队列爆
cron 默认把任务的 stdout + stderr 邮件发给该用户。绝大多数生产环境没装 mail:
- 输出累积到
/var/spool/mail/<user>或/var/mail/<user> - 单条任务输出 1MB × 一天 1440 次 × 几年 → 文件几 GB
- 磁盘满了之后 cron 也跑不动
修复:所有 crontab 任务必须显式重定向输出:
* * * * * /home/me/job.sh >> /var/log/myjob.log 2>&1
# 不关心输出(不推荐,丢失调试信息):
* * * * * /home/me/job.sh > /dev/null 2>&1
# 滚动日志:
* * * * * /home/me/job.sh 2>&1 | logger -t myjob
H. 脚本在 sh 下行为不同
cron 调用的 /bin/sh 在 Debian/Ubuntu 上默认是 dash 不是 bash——dash 不支持 [[ ]]、<<<、{1..10}、数组等 bash 扩展。
症状:shell 里跑得好的脚本,cron 跑挂在某行语法错。
修复:
#!/bin/bash ← 脚本第一行
# 或者 crontab 里强制 bash:
* * * * * /bin/bash /home/me/job.sh
I. 工作目录不是你以为的
cron 任务的 cwd 是该用户的 home 目录(不是脚本所在目录)。
症状:脚本里用相对路径读 ./config.yml 找不到。
修复:脚本里先 cd:
#!/bin/bash
cd "$(dirname "$0")" || exit 1 # 切到脚本所在目录
./mycommand
或者全用绝对路径。
一份”cron 不跑”快速排查 checklist
按这个顺序逐项过,5 分钟定位 90% 问题:
| # | 检查项 | 命令 |
|---|---|---|
| 1 | cron 服务在跑? | systemctl status cron |
| 2 | 在预期时间附近 cron 日志有 CMD 记录? | grep CRON /var/log/syslog | tail |
| 3 | crontab 文件结尾有换行? | tail -c 1 /etc/cron.d/myjob | xxd(应见 0a) |
| 4 | 用 crontab -l 看到的表达式与预期一致? | crontab -l -u me |
| 5 | 命令是否用绝对路径? | 肉眼检查 |
| 6 | 是否所有 % 都转义为 \%? | grep % /etc/crontab |
| 7 | 是否给命令加了 >> log 2>&1 重定向? | 肉眼检查 |
| 8 | 把命令复制出来用 sh -c "..." 手动跑能成? | 测试 |
| 9 | 脚本第一行 shebang 是 #!/bin/bash 不是 #!/bin/sh? | 检查脚本 |
替代方案:systemd timer 更现代
cron 是 1975 年的设计,systemd timer 是 2010 年代的现代实现。新系统强烈推荐用 systemd timer:
| 能力 | cron | systemd timer |
|---|---|---|
| 错过后补跑(机器关机时) | ❌ | ✅ Persistent=true |
| 错峰随机延迟 | ❌ | ✅ RandomizedDelaySec= |
| 资源限制(CPU / IO / 内存) | ❌ | ✅ |
| 依赖其它服务 | ❌ | ✅ Requires= |
| 日志统一 | ❌ 多处 | ✅ journalctl -u myjob |
| 学习成本 | 低 | 中 |
写一个 .timer + .service 文件即可——本工具未提供 systemd timer 生成器,但 cron 表达式可以直接用 OnCalendar= 字段,语义大部分对得上。
默认不做的事
- 不模拟服务器行为——本工具只生成表达式 + 预测下次执行时间,跑不跑要看服务器
- 不识别 Quartz / Spring 7 字段格式——本工具默认 5 字段,需要 7 字段(带秒、年)请额外加字段
- 不验证表达式的”实际跑通” ——能在工具上算出 next run time 不代表服务器上跑得通,要按本文 checklist 排查
写完 cron 表达式 → 用本工具验证下次执行时间 → 贴到 crontab → 务必加 >> log 2>&1 重定向——第一次跑不通时直接看日志,比猜原因快 10 倍。
如果定时任务跑的是数据处理脚本,用 [[duckdb-sql]] 当数据工作台;如果是文件归档脚本,用 [[archive-zip]] / [[archive-extract]] 写好流程再丢 cron;如果定时跑测试或健康检查,改用 systemd timer 或 GitHub Actions 的 schedule 触发器比 cron 稳得多。