定时任务最让人抓狂的一类问题:crontab -l 里明明有这条任务,脚本手动跑完全正常,日志文件就是空的——甚至连文件都没生成。

这篇文章把「日志没写」拆成 7 个具体原因,每个都配有可复现的现象和判断方法。文中所有结论都在 Ubuntu 24.04 + cron 3.0 上实测过,不是凭印象写的。

第 0 步:先确认 cron 到底有没有触发

不要一上来就改脚本。先在系统日志里搜一次:

# Debian / Ubuntu
sudo grep CRON /var/log/syslog | tail -20

# CentOS / RHEL(默认走 journald)
sudo journalctl -u crond --since "10 min ago"

# 部分发行版用 cron 而非 crond
sudo journalctl -u cron --since "10 min ago"

看到这样的行,说明 cron 已经调度了:

CRON[1031]: (jsx13) CMD (echo $(date +)

注意上面这行本身就有问题——命令被截断了,原因见坑 4。

如果一行都没有,别怀疑脚本,问题在调度侧:表达式写错、cron 服务没启动,或者跑在容器 / WSL 里而实例已经休眠。

坑 1:输出被 cron 直接丢弃

这是最常见的一条。cron 执行任务时,会把 stdout 和 stderr 收集起来,通过本地邮件发给用户。服务器没装 MTA(邮件服务)时,这些输出就被扔掉了。

实测的 syslog 记录(跑了 80 秒,出现 32 次):

(CRON) info (No MTA installed, discarding output)

意思是:脚本确实跑了,输出确实产生了,然后被扔了。你去看日志文件,当然什么都没有。

两种修法,建议都做:

# 1. crontab 行尾加重定向(捕获 cron 执行层输出,含 Permission denied 这类报错)
0 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# 2. 脚本内部也重定向(保证脚本自己的逻辑输出不丢)
exec >>/var/log/backup.log 2>&1

exec >>file 2>&1 放在脚本开头,之后整个脚本的所有输出都会自动追加进文件,不用在每个命令后面写重定向。这是最省事的一招。

坑 2:PATH 不全,命令根本找不到

cron 不会加载你的 .bashrc,它的 PATH 是守护进程继承来的,比登录 shell 窄得多。

实测对比(同一台机器):

cron 的 PATH:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

登录 shell 的 PATH:
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
:/usr/lib/wsl/lib:/mnt/d/soft/...(几十个自定义路径)

脚本里 which node 在 cron 下返回 none,在登录 shell 里能找到。同理,docker、kubectl、nvm 装的 node、~/.local/bin 里的工具,在 cron 下全部不存在。

典型症状:脚本第 3 行就退出,日志里只有一句 command not found——而这句话又被坑 1 吞掉了。

修法:

#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

或者在脚本里用命令的绝对路径,比如 /usr/bin/java。

坑 3:工作目录不是脚本目录

cron 执行任务时,工作目录是用户的家目录,不是脚本所在目录。

实测:

CWD=/home/jsx13
cat: ./config.ini: No such file or directory

脚本里写 ./config.ini、./logs/app.log、../data/xxx,全部失效。更隐蔽的是:日志「看起来没写」,其实写到了 $HOME/logs/ 下面。

修法:进目录再干活。

cd "$(dirname "$0")" || exit 1

或者全部用绝对路径。生产脚本建议直接写死绝对路径,最不容易出错。

坑 4:crontab 里的 % 没有转义

这是 cron 最经典的坑,也最容易漏。crontab 把 % 当作换行符,第一个未转义的 % 之后的内容会被当成标准输入,命令本身被截断。

实测:写了这样一行

* * * * * echo $(date +%F) >> /tmp/today.log 2>&1

syslog 里的实际执行记录是:

CRON[1031]: (jsx13) CMD (echo $(date +)

命令在 % 处被切断了,F) >> /tmp/today.log 变成了标准输入。结果就是:任务跑了,报错,日志文件压根没生成。

修法:% 写成 \%。

0 3 * * * echo $(date +\%F) >> /var/log/backup/daily.log 2>&1

实测转义后正常输出:

2026-10-30
2026-10-30

顺带一提,date 的格式化串在 crontab 里几乎必然出现,所以这条坑命中率极高。

坑 5:脚本没有执行权限

chmod +x 忘了做,cron 直接调用脚本时会报 Permission denied。

实测:一个 644 权限的脚本,cron 调用后什么都没发生,目标文件没有生成。手动执行才看到报错:

Permission denied

因为报错走 stderr,又被坑 1 吞掉了,所以表面上「静默失败」。

修法:

chmod +x /opt/scripts/backup.sh

还有个容易忽略的:目录本身也需要 x 权限,否则 cron 进不去。

坑 6:shebang 带了 Windows 换行

脚本在 Windows 上编辑过、或者用 FTP / SMB 拷进去,文件可能带 CRLF 换行。第一行变成 #!/bin/bash\r,内核找不到这个「解释器名带空格和回车」的路径。

实测报错:

cannot execute: required file not found

如果直接 bash script.sh 执行,则会在后面某行报更奇怪的错:

line 2: 1: ambiguous redirect

因为 \r 混进了重定向语法。

修法:

sed -i 's/\r$//' script.sh
# 或者
dos2unix script.sh

确认是否带 CRLF:

file script.sh                          # 出现 "with CRLF line terminators" 就是有问题
head -1 script.sh | od -c | head -2     # 能看到 \r \n

坑 7:cron 根本没跑起来

这条容易被忽略,尤其是开发机的 WSL 或容器环境。

  • 服务没启动:systemctl status cron,没 active 就 sudo systemctl enable --now cron
  • WSL 实例休眠:WSL 的 VM 在没有进程时会被回收,cron 跟着暂停。测试定时任务时要保持一个进程(比如 sleep 300)让实例活着
  • 表达式本身错了:段数对不上(Linux 5 段 vs Quartz 6 段 vs Spring 6 段)、字段取值越界,cron 会静默跳过。用 Cron 表达式在线解析 对一下,能看到未来 5 次执行时间

排查模板:按顺序做,5 分钟内定位

  1. 看 syslog 有没有 CMD (...):没有 → 问题在调度(坑 7);有但命令被截断 → 坑 4
  2. 看 syslog 有没有 No MTA installed:有 → 坑 1,输出被丢了
  3. 把 crontab 行尾加 >> /tmp/x.log 2>&1 再等一轮,这时能看到 Permission denied、command not found 这类真实报错
  4. 用 cron 的环境手动跑一遍(最有效的一步):
sudo -u app env -i HOME=/home/app PATH=/usr/bin:/bin SHELL=/bin/sh \
  /bin/sh -c "/home/app/bin/backup.sh"

env -i 清空所有环境变量,只给最小 PATH——和 cron 的环境基本一致。能复现就在本地调,不能复现再回去查 cron 配置。

生产可用的写法

crontab:

# 分 时 日 月 周   命令(路径全绝对 + 行尾重定向)
0 3 * * * /opt/app/bin/backup.sh >> /opt/app/logs/backup.log 2>&1

脚本:

#!/bin/bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd "$(dirname "$0")" || exit 1
exec >>"$HOME/logs/run.log" 2>&1

echo "[$(date +%F\ %T)] start"
# ... 业务逻辑
echo "[$(date +%F\ %T)] done"

四件事一次做完:PATH 补全、目录锁定、输出落文件、出错即停。这样即使出问题,日志里一定有痕迹,不用再猜。

相关工具与速查