Linux下crontab定时任务不执行的排查与解决方法(PATH环境变量、百分号转义与日志定位)

脚本手动跑得好好的,写进crontab就是不执行,或者执行了但结果是空的。这两种都遇到过,原因基本集中在环境变量、转义和crontab文件本身这三处,跟cron语法反而关系不大。

先确认一件事:cron到底有没有触发这条任务。没触发和触发了但脚本失败,排查方向完全不一样。

cron服务在不在跑

Debian、Ubuntu上服务名叫cron,RHEL、CentOS、Rocky上叫crond,名字不一样,systemctl查的时候别写错。发行差别只在包名和服务名:Debian 12、Ubuntu 24.04,装的是cron包;RHEL 9.4、9.5,装的是cronie,服务名一律crond。

systemctl status cron        # Ubuntu 22.04、24.04
systemctl status crond       # RHEL 8.10、9.4、CentOS 7.9
systemctl enable --now cron

容器里默认不装cron,也不跑systemd,docker镜像里指望cron生效的,得自己在entrypoint里拉起crond。

日志在哪

systemd的发行版直接看journal就行,比翻文件快:

journalctl -u cron --since "30 min ago"
journalctl -u crond -f

RHEL家族有独立日志文件/var/log/cron;Debian、Ubuntu走syslog,装了rsyslog才有/var/log/syslog,没装就只剩journal。一条正常触发的记录长这样:

grep CRON /var/log/syslog | tail -20
# Sep 24 03:10:01 host CRON[28451]: (root) CMD (/opt/job/backup.sh)

能看到CMD那一行,说明cron触发了,问题在脚本里;连CMD都没有,问题在crontab文件本身。

crontab文件的几个硬规矩

1、文件末尾必须有换行。用echo追加生成的crontab,最后一行没有\n的话cron直接不认,日志里也不报错,纯靠肉眼很难发现:

crontab -l | tail -c 20 | xxd | tail -2   # 结尾看不到 0a 就是缺换行

2、百分号要转义。%在crontab里表示换行,命令里所有%都得写成\%。用date生成文件名的写法最容易中招:

# 错:cron会把 %F 当成换行,命令被截断
0 3 * * * cp /data/a.db /bak/a_$(date +%F).db
# 对
0 3 * * * cp /data/a.db /bak/a_$(date +\%F).db

截断后的命令常常还能跑出个结果,只是文件名不对,所以现象很迷惑人。

3、用户字段别写错。crontab -e编辑的是5段(分 时 日 月 周 命令),而/etc/crontab和/etc/cron.d里的文件是6段,命令前面多一个用户名:

# /etc/cron.d/myjob
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * root /opt/job/backup.sh

在crontab -e里写6段,cron会把用户名当命令执行,日志里出现"not found"之类的报错。反过来在/etc/cron.d里写5段,命令就被当成用户名了。

4、/etc/cron.d下的文件属主必须是root,权限不能有组写或其他人写。踩过一次,图省事chmod 666,cron直接拒绝加载,日志里写着INSECURE MODE:

chown root:root /etc/cron.d/myjob
chmod 644 /etc/cron.d/myjob

环境变量才是大头

cron给任务的PATH只有/usr/bin:/bin,SHELL是/bin/sh。手动执行时用的python3.11在/usr/local/bin下、conda环境在/root/miniconda3下、node在/usr/local/node/bin下,这些路径cron一概不知道。脚本一跑就报command not found,而日志又只在邮件里,表面上就是"没执行"。

三种改法,任选一种:

crontab顶部显式声明PATH,对当前用户所有任务生效:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
0 3 * * * /opt/job/backup.sh

命令里全用绝对路径,简单粗暴也最不容易出错:

0 3 * * * /usr/local/bin/python3.11 /opt/job/sync.py >> /var/log/sync.log 2>&1

脚本内部自己补齐环境,适合依赖比较多的场景:

#!/bin/bash
source /etc/profile
source /root/.bashrc 2>/dev/null
cd /opt/job || exit 1
/usr/local/bin/python3.11 sync.py

cron不会自动加载~/.bashrc和/etc/profile,这跟登录shell不一样,写在那些文件里的export在cron里一律拿不到。

MAILTO=""那一行建议加上。不加的话cron会把输出寄给本地用户,机器上没装MTA,邮件就堆在/var/mail或者干脆丢掉,报错信息一条都看不到。

脚本自身的问题

手动能跑、cron跑不了,还有一小部分是脚本本身。查这几个点:

有没有执行权限,chmod +x /opt/job/backup.sh。shebang写对没有,用到数组、[[ ]]就得是#!/bin/bash而不是#!/bin/sh。文件是不是从Windows传过来的CRLF行尾,这个报错很典型:

/bin/bash^M: bad interpreter: No such file or directory
# 修:
sed -i 's/\r$//' /opt/job/backup.sh

脚本里的相对路径也别用。cron执行时的工作目录是用户的home,不是脚本所在目录,写./config.ini读到的根本不是想要的那个文件。

时间对不上

cron按系统时区跑,容器默认是UTC,宿主机是CST,看起来就像任务晚执行了8小时。查一下:

timedatectl | grep "Time zone"
date

改完时区要重启cron才生效,这是实测确认的,只改timedatectl不重启,任务还是按老时区触发:

timedatectl set-timezone Asia/Shanghai
systemctl restart cron

一套能直接抄的排查顺序

碰到任务没跑,按这个来,基本几分钟内能定位:

systemctl status cron                       # 1 服务活着吗
crontab -l                                  # 2 任务写进去了吗
journalctl -u cron --since "1 hour ago"     # 3 触发了吗,报什么错
/bin/bash -x /opt/job/backup.sh             # 4 单独跑一遍看过程
env -i /bin/bash -c /opt/job/backup.sh      # 5 空环境跑,模拟cron的真实环境

第5步最有用。env -i会清掉所有环境变量,跑不通就说明脚本依赖了外部变量,表现出来的正是"手动可以、cron不行"。

数据截至2026年9月,以上在Ubuntu 22.04、24.04与RHEL 8.10、9.4、9.5,都验证过。