故障排查 · 2026 年 11 月 11 日

定时任务没执行?cron 环境变量与 PATH 的坑

手动跑正常,crontab 里却静默失败。cron 不读你的 .bashrc,PATH 只有两个目录,工作目录还是家目录。本文给出三分钟定位方法和一个自带环境的脚本模板。

凌晨三点的备份任务,手动执行一次成功,放进 crontab 之后连续一周没有产物。翻 /var/log/cron,执行记录明明白白写着「已运行」。

问题不在 cron 有没有触发,而在它以什么环境触发。cron 启动的是一个非交互、非登录的 shell,你 ~/.bashrc 里那些 export PATH、conda activate、别名,它一概看不到。

现象:三种失败,三种原因#

现象 大概率原因
日志里完全没有执行记录 cron 服务没启动、语法错误、文件权限或 CRLF 换行
有执行记录,但没有任何产物 工作目录不对、相对路径失效、权限不足
报 command not found PATH 里没有 /usr/local/bin、nvm、conda
脚本中途退出,日志只有半截 用了 set -e 且某条命令失败;或 % 未转义被截断
输出消失 stdout/stderr 没重定向,被当成邮件投递或丢弃

第二种最坑:脚本里写 gzip > backup.gz,cron 的工作目录是家目录,文件其实生成了,只是不在你以为的地方。

容器里跑 cron 还有两个额外问题:多数基础镜像没有 syslog,日志必须自己重定向;镜像默认时区往往不是 UTC+8,0 3 * * * 实际会在本地时间上午十一点执行,而日志看起来完全正常。

还有一种「假成功」:任务跑了,退出码 0,但业务数据没变。常见于脚本里某条命令失败但没有 set -e,后面继续执行,最后正常退出。cron 只记「跑过了」,不记「跑对了」。

权限问题也常被忽略:用 root 的 crontab 跑本应属于业务用户的脚本,产物属主变成 root,后续普通用户读不到;反过来,普通用户的 crontab 访问不了 /root 下的密钥文件。

定位:把 cron 的真实环境导出来#

最直接的办法是让 cron 自己交代。临时加一行,一分钟内就能拿到:

bash
# 每分钟把 cron 的环境落盘,观察完记得删掉
* * * * * env > /tmp/cron.env 2>&1; echo "PWD=$PWD" >> /tmp/cron.env
# 对比交互式登录 shell 的环境
diff <(sort /tmp/cron.env) <(env -i bash -lc 'env' | sort) | head -40

差异通常集中在 PATH、HOME、SHELL、LANG 这几项。看到 PATH=/usr/bin:/bin 就基本能确认:你的 node、python3、mysqldump 如果装在 /usr/local/bin 或用户目录下,cron 找不到。

再看 cron 自己的日志,确认有没有触发:

bash
grep CRON /var/log/syslog | tail -20      # Debian / Ubuntu
journalctl -u cron --since '10 min ago'    # systemd 系统
tail -50 /var/log/cron                     # RHEL / CentOS
systemctl status cron                       # 或 crond
crontab -l

如果日志里根本没有这一行,先查服务状态和语法;如果行尾是 CMD (...),说明命令已经交给 shell 了,问题在环境或脚本内部。

按这张清单逐项对照,一轮就能定位:

  • PATH 是否包含脚本里用到的命令(which node、which mysqldump)
  • 脚本是否显式 cd 到正确目录
  • 输出是否重定向到了可写路径
  • 容器时区是否与预期一致
  • 上一次是否还没结束,导致本次被锁挡住

想要执行轨迹而不只是结果,把命令换成 /bin/bash -x /opt/scripts/x.sh,或者干脆在脚本里用 set -x 临时打开。cron 的日志只告诉你「执行了」,-x 才告诉你「执行到哪一步」。

根因:cron 用的是最小环境#

cron 执行任务时只注入很少的变量:SHELL 一般是 /bin/sh,PATH 通常是 /usr/bin:/bin,HOME 是用户家目录,LOGNAME 是用户名。它不会加载 .bashrc、.bash_profile、.profile。

由此引出四个连锁问题:

  1. 版本管理工具装的可执行文件不在 PATH 里,node、npm、python 全部找不到。
  2. 工作目录不是脚本所在目录,相对路径写到别处,或者直接因目录不存在而失败。
  3. 时区取系统时区。容器里常是 UTC,你以为的凌晨三点其实是上午十一点。
  4. 环境变量没有继承。数据库密码、代理设置、LANG 全部缺失,程序可能因编码问题报错。

还有三个纯配置坑。% 在 crontab 里有特殊含义,未转义时被当成换行符,命令从中间截断,而语法检查不会报错;文件用 Windows 编辑器保存、带 CRLF 换行时,每行末尾多一个 \r,命令变成 xxx.sh\r;/etc/cron.d 下的文件名带点通常会被忽略,配置放了却不生效。

命令没输出也别急着断定没跑。默认情况下 cron 会把输出通过邮件发给用户,机器上没配 MTA 时就直接丢弃。加一行 MAILTO="" 或者重定向到文件,才能留住现场。

修复:让脚本自带环境#

原则是:不依赖调用者的环境,脚本自己把 PATH、目录、日志、锁全部定死。

bash
#!/usr/bin/env bash
set -euo pipefail

# 1) 显式 PATH,覆盖 cron 的最小环境
export PATH="/usr/local/bin:/usr/bin:/bin:/opt/app/bin"
export LANG="C.UTF-8"

# 2) 固定工作目录,相对路径才有意义
cd /opt/app

# 3) 所有输出进日志,不依赖邮件
exec >>/var/log/app-backup.log 2>&1
echo "[$(date -Is)] start"

# 4) 防重叠:上一次没跑完就跳过,并说明原因
flock -n /tmp/app-backup.lock -c '
  /usr/bin/mysqldump --defaults-file=/opt/app/my.cnf app \
    | gzip > /opt/app/backup/db-$(date +%F).sql.gz
' || echo "skip: previous run still active"

对应的 crontab 用绝对路径调用,并显式重定向。环境变量也可以写在文件顶部,Vixie cron 与 cronie 都支持:

cron
# crontab 顶部声明环境
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=""

# 分 时 日 月 周  命令
30 2 * * *  /bin/bash /opt/scripts/app-backup.sh >> /var/log/app-backup.cron.log 2>&1

脚本里用 flock -n 而不是 flock -w,是为了让重叠的那一次直接跳过,而不是排队堆积。备份任务排队几次之后,往往就跑不完了。

调用时用绝对路径的解释器更稳:/bin/bash /opt/scripts/x.sh 不依赖 shebang 和执行位,挂载卷导致的权限丢失也不会让任务静默失败。

日志本身也要管:exec >> 追加写入的文件不会自动切割,跑一年就是几个 GB。用 logrotate 给它配一份规则,或者按月分文件,别等磁盘满了才发现。

密码不要直接写进脚本。mysqldump 用 --defaults-file 指向权限 600 的配置文件,比在命令行里写 -p密码 安全,也不会出现在 ps 的输出里。

防复发#

  • 统一脚本模板:PATH、cd、日志重定向、flock、set -euo pipefail 五件套。
  • 上线前用最小环境模拟一次:env -i /bin/sh -c '/opt/scripts/app-backup.sh',能过再进 crontab。
  • 加心跳:脚本成功结束前打一条日志或请求一个探针地址,监控没收到就说明没跑。
  • 能换就换:systemd timer 支持 Environment=、WorkingDirectory=、OnCalendar=,日志进 journal,排查成本低得多。
  • 容器场景显式设置 TZ,并在脚本里打印 date 用于核对时区。
  • 把 crontab 纳入版本管理,别只在服务器上 crontab -e。

结论#

cron 不执行,先分清是「没触发」还是「触发了但失败」。看日志有没有记录,两分钟就能判断。

触发了但失败,九成是环境问题:PATH 只有 /usr/bin:/bin,工作目录是家目录,.bashrc 根本没被读取。让脚本自带 PATH 和 cd,把输出重定向到文件,问题就收敛了。

两个高频细节别忘:crontab 里的 % 必须转义成 \%;容器里先确认时区,否则任务会在错误的时间「成功」执行。

判断任务是否真的成功,别只看 exit code。加一条心跳上报,比翻日志可靠。

完

#Linux#cron#排障
回到顶部 ↑