Cron 表达式写得没错任务却不跑——9 类"配置正确但不触发"的真实原因与排查路径

· 约 6 分钟 Cron

用本工具把 cron 表达式拼出来、确认下次执行时间符合预期、贴到服务器 crontab 里——本以为大功告成,结果定时任务一次也没跑

这种”表达式正确但任务不触发”是 cron 最让人崩溃的场景。本文不再讲表达式语法(已有 Cron 5 分钟入门时区 / DST 陷阱),专攻”配置上去不跑”的 9 类原因和排查路径。

第一步:看日志确认 cron 有没有起任务

90% 的”不跑”问题,在日志里就有答案

系统cron 系统日志路径
Debian / Ubuntu/var/log/syslog(过滤 grep CRON
CentOS / RHEL / Rocky/var/log/cron
macOSlog 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 弄反

位置编辑方式用户身份
用户 crontabcrontab -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% 问题:

#检查项命令
1cron 服务在跑?systemctl status cron
2在预期时间附近 cron 日志有 CMD 记录?grep CRON /var/log/syslog | tail
3crontab 文件结尾有换行?tail -c 1 /etc/cron.d/myjob | xxd(应见 0a)
4crontab -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

能力cronsystemd 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 稳得多。

❓ 常见问题

表达式我用本工具验证下次执行时间都对,但服务器上 crontab 不触发,怎么回事?

99% 不是表达式问题——cron 表达式只决定"什么时间触发",不决定"触发后能不能跑通"。日常踩坑的真实原因:(1) PATH 环境变量缺失——cron 默认 PATH 是 /usr/bin:/bin,shell 里能跑的 python node mysql 直接 command not found。(2) 用户身份不对——你在 user crontab 里写的任务以该用户身份跑,但脚本里读的文件 / 写的目录权限不对就静默失败。(3) 文件结尾没换行符——POSIX 规定 crontab 文件必须以换行结尾,缺了最后一个任务被吞。(4) 命令里有 %——cron 把 % 当 stdin 分隔符,必须转义成 \%。先用 grep CRON /var/log/syslogjournalctl -u cron 看日志,把"为什么没跑"具体化。

怎么看 cron 任务有没有跑?日志在哪里?

三层日志:(1) 系统 cron 日志——Debian/Ubuntu 在 /var/log/syslog,过滤 grep CRON /var/log/syslog;CentOS/RHEL 在 /var/log/cron。这里记录"cron 是否在某时刻调起了任务",不记录任务输出。(2) 任务自己的输出——cron 默认把 stdout / stderr 邮件发给该用户(需要本地 mail 服务),但绝大多数生产环境没装 mail,输出实际去了 /var/spool/mail/<user> 或直接丢弃。要看输出必须在 crontab 里手动重定向* * * * * /path/to/script.sh >> /var/log/myjob.log 2>&1。(3) systemd timer 日志——如果用的不是 cron 而是 systemd timer,journalctl -u myjob.timer 查。

cron 里的命令为什么经常报 "command not found"?

PATH 太短。你登录 shell 时 PATH 通常是 /usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/sbin:/bin:/snap/bin:~/bin,但 cron 默认 PATH 只有 /usr/bin:/bin——所以 python3(在 /usr/local/bin)、node(在 nvm 路径)、mysql(在 /usr/local/mysql/bin)全都找不到。3 种修复方式(推荐顺序):(1) 用绝对路径/usr/bin/python3 /home/me/job.py——最稳,无环境依赖。(2) 在 crontab 顶部设 PATHPATH=/usr/local/bin:/usr/bin:/bin:/home/me/bin——影响下面所有任务。(3) 包装成 shell 脚本:脚本内部 source ~/.bashrcsource /etc/profile——把交互式 shell 的完整环境拉进来。生产推荐 (1),省心。

"分 时 日 月 周"五个字段我写了"15 8 1,15 * 1-5",结果每月 1 号、15 号 + 每个周一到周五都跑了,重复执行,怎么回事?

这是 cron 的"日 OR 周"规则,不是 bug。当日字段和周字段都不是 *时,cron 触发条件是"日匹配 OR 周匹配"(而非 AND)。所以 15 8 1,15 * 1-5 意味着"每月 1/15 日触发 OR 每周一到周五触发",每个工作日都跑,1/15 日多跑一次。3 种修复:(1) 只用日,不用周15 8 1,15 * *——每月 1 日和 15 日 8:15 执行。(2) 只用周,不用日15 8 * * 1-5——每周一到周五 8:15。(3) 真要"工作日 + 月初月中"AND:cron 做不到,要在脚本里加判断 [ $(date +%d) -eq 1 ] || [ $(date +%d) -eq 15 ] || ...,或者用 Anacron / systemd timer 的 Persistent=true 灵活配置。

"@reboot"和"@daily"这种宏定义可以用吗?

可以但有坑。POSIX 标准 cron 不支持 @reboot @yearly @monthly @weekly @daily @hourly——但 Linux 的 Vixie cron / cronie 实现都加了这些。使用前确认你的 cron 实现cron -Vcrontab -V 看版本。@reboot 的特殊行为:(1) 在 cron 服务启动时触发,不是系统启动时——所以如果 cron 服务被禁用 / 后启动,@reboot 任务可能不跑。(2) 早期版本的 @reboot 对 user crontab 不生效,要写到 /etc/cron.d/ 下。(3) 容器场景下(Docker)@reboot 几乎不可靠,容器启动一次性任务请用 entrypoint 脚本而不是 cron。@daily 等同 0 0 * * *——午夜 0:00 触发,扎堆——多机部署时建议加上 Sleep $((RANDOM % 3600)) 错峰。

crontab 里的 % 字符直接被截断,命令变短了,怎么回事?

cron 把未转义的 % 当 stdin 分隔符——第一个 % 之后的所有内容都被当成命令的标准输入,不是命令本身。最常见踩坑:date +%Y-%m-%d 在 crontab 里执行结果是 date +(被截断在第一个 %)。修复:所有 % 都转义为 \%date +\%Y-\%m-\%d实际效果:(1) \% 在 crontab 里被解释成字面的 %,命令拿到的就是 date +%Y-%m-%d,正常输出。(2) % 如果故意当 stdin 用(极少场景):echo "abc" | wc -l %\nhello\nworld 这种姿势,第一个 % 后面的 \nhello\nworld 会作为 wc 的输入。绝大多数任务:crontab 里出现 % 一律转义。

打开 Cron 解析 · 可视化生成 · 示例库

📖 同一工具的其他教程