定时任务最让人抓狂的一类问题:crontab -l 里明明有这条任务,脚本手动跑完全正常,日志文件就是空的------甚至连文件都没生成。
我把「日志没写」拆成 7 个具体原因,每个都在 Ubuntu 24.04 + cron 3.0 上实测复现过,不是凭印象写的。
第 0 步:先确认 cron 到底有没有触发
不要一上来就改脚本。先在系统日志里搜一次:
bash
# Debian / Ubuntu
sudo grep CRON /var/log/syslog | tail -20
# CentOS / RHEL(默认走 journald)
sudo journalctl -u crond --since "10 min ago"
看到这样的行,说明 cron 已经调度了:
bash
CRON[1031]: (jsx13) CMD (echo $(date +)
注意上面这行本身就有问题------命令被截断了,原因见第 4 条。
如果一行都没有,别怀疑脚本,问题在调度侧:表达式写错、cron 服务没启动,或者跑在容器 / WSL 里而实例已经休眠。
坑 1:输出被 cron 直接丢弃
这是最常见的一条。cron 执行任务时,会把 stdout 和 stderr 收集起来,通过本地邮件发给用户。服务器没装 MTA(邮件服务)时,这些输出就被扔掉了。
实测的 syslog 记录(跑了 80 秒,出现 32 次):
scss
(CRON) info (No MTA installed, discarding output)
意思是:脚本确实跑了,输出确实产生了,然后被扔了。你去看日志文件,当然什么都没有。
两种修法,建议都做:
bash
# 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 窄得多。
实测对比(同一台机器):
javascript
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 吞掉了。
修法:
bash
#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
坑 3:工作目录不是脚本目录
cron 执行任务时,工作目录是用户的家目录,不是脚本所在目录。
实测:
bash
CWD=/home/jsx13
cat: ./config.ini: No such file or directory
脚本里写 ./config.ini、./logs/app.log、../data/xxx,全部失效。更隐蔽的是:日志「看起来没写」,其实写到了 $HOME/logs/ 下面。
修法:
bash
cd "$(dirname "$0")" || exit 1
坑 4:crontab 里的 % 没有转义
cron 最经典的坑,也最容易漏。crontab 把 % 当作换行符 ,第一个未转义的 % 之后的内容会被当成标准输入,命令本身被截断。
实测写了这样一行:
bash
* * * * * echo $(date +%F) >> /tmp/today.log 2>&1
syslog 里的实际执行记录是:
bash
CRON[1031]: (jsx13) CMD (echo $(date +)
命令在 % 处被切断,F) >> /tmp/today.log 变成了标准输入。任务跑了、报错了、日志文件压根没生成。
修法:% 写成 %。
bash
0 3 * * * echo $(date +%F) >> /var/log/backup/daily.log 2>&1
date 的格式化串在 crontab 里几乎必然出现,所以这条命中率极高。
坑 5:脚本没有执行权限
chmod +x 忘了做,cron 直接调用脚本时会报 Permission denied。
实测:一个 644 权限的脚本,cron 调用后什么都没发生。手动执行才看到报错:
Permission denied
因为报错走 stderr,又被坑 1 吞掉了,所以表面上「静默失败」。
修法:
bash
chmod +x /opt/scripts/backup.sh
还有个容易忽略的:目录本身也需要 x 权限。
坑 6:shebang 带了 Windows 换行
脚本在 Windows 上编辑过、或用 FTP / SMB 拷进去,文件可能带 CRLF 换行。第一行变成 #!/bin/bash\r,内核找不到这个「解释器名带回车」的路径。
实测报错:
lua
cannot execute: required file not found
如果直接 bash script.sh 执行,则会在后面某行报更奇怪的错:
arduino
line 2: 1: ambiguous redirect
因为 \r 混进了重定向语法。
修法:
bash
sed -i 's/\r$//' script.sh
# 或者
dos2unix script.sh
确认是否带 CRLF:
bash
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 实例休眠:VM 在没有进程时被回收,cron 跟着暂停。测试时要保持一个进程让实例活着
- 表达式本身错了:段数对不上(Linux 5 段 vs Quartz 6 段)、字段越界,cron 会静默跳过。用在线 cron 解析工具核对一下未来 5 次执行时间
排查顺序:5 分钟内定位
- 看 syslog 有没有
CMD (...):没有 → 问题在调度;有但命令被截断 → 坑 4 - 看 syslog 有没有
No MTA installed:有 → 坑 1,输出被丢了 - crontab 行尾加
>> /tmp/x.log 2>&1再等一轮,能看到Permission denied、command not found这类真实报错 - 用 cron 的环境手动跑一遍(最有效):
bash
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 环境基本一致。能复现就在本地调。
生产可用的写法
crontab:
perl
# 分 时 日 月 周 命令(路径全绝对 + 行尾重定向)
0 3 * * * /opt/app/bin/backup.sh >> /opt/app/logs/backup.log 2>&1
脚本:
bash
#!/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 补全、目录锁定、输出落文件、出错即停。这样即使出问题,日志里一定有痕迹。
延伸阅读
同一批实测过的内容还有:Bash 调试 10 招(set -x 怎么局部开关)、Cron 表达式段数与周字段差异(Linux / Spring / Quartz)。