本文探讨了Linux定时任务crontab不执行的原因,分析了环境变量缺失、工作路径与时区问题。作者分享了排查经验,包括检查cron日志、确认环境变量PATH和当前工作目录,并提供了解决方法,如显式声明PATH或使用绝对路径。
写好的 Shell 脚本或者 Python 数据同步任务,在终端里敲 ./backup.sh 跑得顺顺当当。兴高采烈把命令配到 crontab -e 里面,准备让它每天凌晨自动跑,结果第二天早上到服务器上一查,预期的日志文件压根没生成,数据库备份也没动静,就好像这条定时任务从来没存在过一样。
这种事情几乎每个管过 Linux 服务器的人都遇到过。最抓狂的地方在于,终端手动跑完全没有报错,cron 却静悄悄地不干活,连个抛错弹窗都没有。排查几次之后就会发现,cron 的运行机制和我们平时交互式登录的终端完全不是一码事。
这里把我这几年在生产服务器排查 crontab 踩过的 6 个典型暗坑整理出来,顺带理清楚排查这类问题的标准顺序。
第一步:先看 cron 守护进程到底有没有调度它
脚本没产生预期结果,先不要急着改脚本代码,第一件事是确定 cron 到底有没有按时触发这个任务。
如果是 Debian 或 Ubuntu 系统,cron 的执行记录默认记录在 /var/log/syslog:
grep CRON /var/log/syslog | tail -n 20
或者通过 systemd 日志查看:
journalctl -u cron -n 30 --no-pager
如果是 CentOS、RHEL 或 Rocky Linux,日志在 /var/log/cron:
tail -n 20 /var/log/cron
检查日志里有没有类似下面这样的记录:
Oct 4 02:00:01 server CRON[12345]: (root) CMD (/opt/scripts/backup.sh)
看到 CMD (...) 说明 cron 服务本身正常,而且在指定时间点确实拉起了命令。如果日志里根本没有任何关于这个任务的记录,问题大概率出在 crontab 配置语法、cron 守护进程未启动、或者文件末尾格式上;如果日志明确记录了拉起,但最终没结果,那 100% 是脚本执行环境出了问题。
坑一:极简环境变量 PATH 导致找不到命令
这是导致 crontab 不执行最高频的原因,没有之一。
很多脚本在终端手动跑能通,是因为我们登录后加载了 /etc/profile、~/.bashrc 或者 ~/.bash_profile,系统的 PATH 包含 /usr/local/bin、/usr/local/sbin 以及当前用户的各种工具路径(比如 Python 虚拟环境、Node.js 的 npm 全局路径、Docker 路径)。
但 cron 执行任务时,属于非交互式、非登录 shell。它默认提供的环境变量极度干净,PATH 通常仅有:
/usr/bin:/bin
如果脚本里写了类似 python3 main.py,而你的 Python 3 装在 /usr/local/bin/python3,或者在脚本里调用了 mysqldump、docker、rsync 等工具,cron 启动它时就会直接报 command not found。
怎么避坑
有两个做法:
方法 A:在 crontab 文件最顶部显式声明完整的 PATH:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
方法 B:在 Shell 脚本内部使用命令的绝对路径,或者在脚本开头手动 source 环境变量:
#!/bin/bash
source /etc/profile
source ~/.bashrc
/usr/local/bin/python3 /opt/myapp/main.py
写定时任务脚本时,把调用的关键二进制路径全部写死成绝对路径(比如可以用 which python3、which mysqldump 确认),能避开绝大部分莫名其妙的报错。
坑二:当前工作目录偏差与相对路径翻车
在终端里调试脚本时,我们习惯先 cd /opt/myapp,然后再执行 ./run.sh。脚本里写的 ./config.json、./data/output.csv 都能按照相对路径顺利读写。
但是一旦交由 cron 接管,cron 启动子进程时的当前工作目录($PWD)默认是该任务所属用户的家目录。如果是 root 用户的 crontab,当前目录就是 /root;如果是普通用户 www,当前目录就是 /home/www。
如果脚本里有这样的代码:
with open("config.yaml", "r") as f:
config = yaml.safe_load(f)
cron 运行它时,会试图在 /root/config.yaml 寻找配置文件,结果直接抛出 FileNotFoundError,整个任务瞬间崩溃退出。
怎么避坑
在 Shell 脚本的第一行之后,强制把工作目录切换到脚本所在目录:
#!/bin/bash
cd "$(dirname "$0")" || exit 1
# 之后的相对路径操作就会完全以当前脚本目录为基准
python3 worker.py
或者在 crontab 规则中先进入目录再执行:
0 3 * * * cd /opt/myapp && /usr/bin/python3 worker.py >> /var/log/myapp.log 2>&1
坑三:标准输出和错误输出被系统静默丢弃
很多人在 crontab 里配完命令就直接收工:
0 2 * * * /opt/scripts/backup.sh
这等于开盲盒。cron 执行任务时,默认会把标准输出(stdout)和错误输出(stderr)捕获起来,尝试通过系统内部邮件程序(sendmail/postfix)发给该用户的本地邮箱。
现在大多数云服务器根本没有装 postfix,或者本地邮件服务没配好。结果就是:脚本即便报了语法错、缺少依赖包、缺少权限,错误信息直接被丢进黑洞,我们在终端里根本看不到任何痕迹。
怎么避坑
每一条 crontab 任务,都必须显式把标准输出和标准错误同时重定向到独立的日志文件:
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
这里的 >> 表示追加写入日志,2>&1 表示将标准错误(文件描述符 2)重定向到标准输出(文件描述符 1)。
如果排查期间怀疑任务没跑,先加上日志重定向。90% 的脚本失败原因在打开日志文件的第一眼就能看明白。
坑四:文件末尾缺少换行符,整行规则直接被忽略
这是一个隐藏得极深、坑过无数老司机的细节。
很多 Linux 系统的 cron 守护进程(尤其是经典的 Vixie cron)在解析 crontab 文件时,是逐行按照行结束符 \n 进行扫描的。如果编辑 crontab 文件时,最后一行配置写完后没有敲回车换行,那这一行就不会包含合法的结束标记。
后果是:cron 在解析时会直接跳过这最后一行,而且不会产生任何报警或者语法提示。你用 crontab -l 还能清晰看到那行任务,但在 cron 引擎眼里它根本不合法。
怎么避坑
每次用 crontab -e 修改定时任务时,在末尾多敲一个回车,保留一个空行。如果用自动化脚本(如 Ansible 或 Shell 里的 echo "..." >> /var/spool/cron/crontabs/root)批量追加任务,必须确认结尾带有换行符:
echo "0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1" >> /var/spool/cron/root
echo "" >> /var/spool/cron/root
坑五:服务器时区是 UTC 导致的“提前或延后 8 小时”
有时候任务其实按部就班跑了,但你以为它没跑,原因是它没在你预期的时间点跑。
国内购买的海外云服务器,或者很多 Docker 基础镜像、精简系统,默认系统时区是 UTC(世界协调时),比北京时间(CST,UTC+8)慢了整整 8 个小时。
你在 crontab 里写了:
0 2 * * * /opt/scripts/daily_report.sh >> /var/log/report.log 2>&1
你以为会在北京时间凌晨 02:00 生成报表,但实际上它在 UTC 02:00(北京时间上午 10:00)才会触发。早上 9 点去查服务器,自然什么都没有。
反过来,如果在系统时区是 UTC 的情况下,你想在北京时间凌晨 2 点执行,那应该换算成 UTC 前一天的 18 点(0 18 * * *)。
怎么避坑
先用命令核实服务器当前的真实时区和时间:
timedatectl
或者:
date -R
如果时区是 UTC 且希望统一改成北京时间,直接调整系统时区:
timedatectl set-timezone Asia/Shanghai
改完时区后,记得重启一下 cron 守护进程,让它重新读取系统时间配置:
systemctl restart cron # Ubuntu/Debian
# 或者
systemctl restart crond # CentOS/RHEL
坑六:任务超时导致并发堆叠,用 flock 做互斥锁
有些定时任务需要拉取数据或处理批量图片,平时几秒钟搞定,配置了每 5 分钟跑一次:
*/5 * * * * /opt/scripts/sync_data.sh >> /var/log/sync.log 2>&1
某天遇到网络波动或者单次数据量骤增,这个脚本跑了整整 8 分钟还没收尾。而第 5 分钟的时候,cron 又毫不犹豫地拉起了第二个实例。
两个进程同时读写同一个数据库表、同一个缓存文件,直接引发死锁和数据脏写;更糟的是如果第二个进程也卡住,第 10 分钟又会起第三个,机器的负载瞬间被压垮。
怎么避坑
定时频繁触发的任务,建议用 Linux 内置的 flock 命令做文件排他锁:
*/5 * * * * /usr/bin/flock -xn /tmp/sync_data.lock -c "/opt/scripts/sync_data.sh >> /var/log/sync.log 2>&1"
这里的参数作用很直接:
-x:获取排他锁(独占锁);-n:非阻塞模式(Non-blocking)。如果发现上一个任务还在跑、锁被占用,当前任务立刻放弃执行并退出,绝不排队堆叠。
任务执行完毕退出后,内核会自动释放文件锁,干净利索。
一份可直接套用的健壮 crontab 编写规范
把上面踩过的坑综合起来,建议以后每次给生产环境加定时任务时,按照下面这个基础骨架来写:
# 1. 显式指定 Shell 解释器与完整的执行路径
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 2. 定时任务模板:进入目录 + 文件锁防堆叠 + 绝对路径 + 标准与错误输出重定向
0 2 * * * cd /opt/scripts && /usr/bin/flock -xn /tmp/backup.lock -c "/usr/bin/python3 backup.py >> /var/log/backup.log 2>&1"
# 3. 文件末尾必须保留一个空行
写完后,先在终端里用 env -i /bin/bash /opt/scripts/backup.sh 模拟一下无环境变量的极端情况跑一次。如果干净环境下也能跑通,并且日志能正常输出,丢进 crontab 之后基本就不会再出幺蛾子。
文章标题:Linux 定时任务 crontab 为什么死活不执行?排查环境变量缺失、工作路径与时区陷阱我踩过的几个坑
文章链接:https://www.llbbs.cn/jishujaocheng/214.html
本站文章均为原创,未经授权请勿用于任何商业用途
评论一下?