手动能跑,cron 不跑:定时任务的五个隐形坑

简介: 本文揭秘定时任务“手动能跑、cron不跑”的五大隐形坑:PATH环境异常、shell配置未加载、工作目录非预期、%符号未转义、输出静默丢失。结合日志排查与实操方案,助你快速定位并规避Linux定时调度常见陷阱。(239字)

原文链接:手动能跑,cron 不跑:定时任务的五个隐形坑 - 码日新 · 每日文章


手动能跑,cron 不跑:定时任务的五个隐形坑

有个脚本,你在终端里敲 ./backup.sh,一切正常。放进 crontab,第二天早上一看,备份目录是空的。

crontab -l 里那行命令没写错,路径也对,可它就是不动。这种问题之所以难查,是因为 cron 执行命令时的环境和你的交互式 shell 差得很远:PATH 更短、不读你的配置、工作目录不是你以为的那个、输出没人接。下面按排查顺序拆开讲。

第一步:先确认 cron 到底执行了没有

别急着改脚本。先分清是“没跑”还是“跑了但失败”。

systemctl status cron          # RHEL 系是 crond
journalctl -u cron --since "10 min ago"
grep CRON /var/log/syslog | tail -5   # RHEL 看 /var/log/cron

日志里出现 (razer) CMD (/home/razer/bin/backup.sh),说明 cron 确实调用了,问题在脚本内部;连这行都没有,就要往前查:

  • 任务是不是写在了别的用户下面?crontab -l 只列当前用户,sudo crontab -l -u www-data 才看得到别人的。
  • 放在 /etc/cron.d/ 里的文件,要求属主是 root、不能组可写,文件名只能包含字母、数字、下划线和连字符,带 .sh 后缀会被直接忽略。
  • cron 服务本身是否在跑。容器里尤其常见——很多精简镜像根本不带 cron。

顺带纠正一个流传很广的说法:改完 crontab -e 不需要重启 cron,保存的那一刻它就会重新读取。

坑一:PATH 只有两个目录

Debian/Ubuntu 的 crontab(5) 明确写了,cron 给任务设置的 PATH 是 /usr/bin:/bin。也就是说:

  • ~/.local/bin 里的 uv、poetry 找不到;
  • /usr/local/bin 里的自定义工具找不到;
  • nvm、pyenv、conda 装的解释器全都找不到。

脚本里写了 python3 xxx.py,结果跑的是系统那个 3.8,而不是你 pyenv global 的 3.12;写了 node,直接 command not found。

两种改法,选一种:

# 方案 A:在 crontab 顶部声明,作用于该文件里所有任务
PATH=/home/razer/.local/bin:/usr/local/bin:/usr/bin:/bin
MAILTO=""

0 3 * * * /home/razer/bin/backup.sh
# 方案 B:脚本里全部用绝对路径,最不容易被环境影响
/usr/bin/python3 /srv/app/job.py

在交互式终端里用 command -v node 查到的路径,就是 cron 里该写的那一个。

坑二:你的 shell 配置根本没被读

cron 默认用 SHELL=/bin/sh(Debian 上指向 dash),而且是非交互、非登录地启动。这意味着:

  • ~/.bashrc 不读——它开头通常就有这段保护:
case $- in
  *i*) ;;
    *) return;;
esac

非交互式直接 return,后面 export 的变量一个都没生效。

  • ~/.profile、~/.bash_profile 也不读,那是登录 shell 才加载的。

所以 nvm use 20、conda activate、export API_KEY=... 这类写在 rc 文件里的东西,在 cron 里统统不存在。

如果确实需要复用一套环境变量,可以单独建一个只放 export 的文件,让 bash 在非交互启动时读取:

# crontab 顶部
SHELL=/bin/bash
BASH_ENV=/home/razer/.cronenv

~/.cronenv 里只写 export FOO=bar,不要 echo 任何东西——它的输出会被拼进命令执行结果里。注意 BASH_ENV 只对 bash 生效,换成 dash 就不管用了。

坑三:工作目录是 HOME,不是脚本目录

脚本里写了 ./config.yml、logs/app.log、../data/,手动跑时因为你在项目目录,一切正常;cron 里工作目录是 $HOME,相对路径全部指向别处,于是“文件不存在”或者日志写到了家目录。

在脚本开头加一行,一劳永逸:

cd "$(dirname "$(readlink -f "$0")")" || exit 1

readlink -f 是为了处理脚本被软链到 /usr/local/bin 的情况,否则 dirname 拿到的是软链所在目录。

坑四:% 在 crontab 里不是百分号

这是最容易被忽略的一条。在 crontab 的命令字段里,未转义的 % 表示换行,而且第一个 % 之后的内容会变成命令的标准输入。于是这行:

0 3 * * * /usr/bin/tar czf /backup/$(date +%F).tar.gz /srv

实际执行的命令在 date + 处就被截断了,剩下的 F)... 被当成 stdin。结果要么报错,要么生成一个名字奇怪的包。

正确写法是转义:

0 3 * * * /usr/bin/tar czf /backup/backup-$(date +\%F).tar.gz /srv

更好的做法是别在 crontab 里写复杂命令,把逻辑塞进脚本,crontab 里只留一行 0 3 * * * /home/razer/bin/backup.sh。这样既能避开 %,也方便本地调试。

坑五:输出没人接,缓冲区还会骗你

cron 里没有终端。命令的输出会被交给本机 MTA 发邮件;如果没装邮件服务,就直接丢弃。所以脚本里一句 echo "start" 既不会出现在任何地方,也不会告诉你出了什么错。

两个动作:

# 1. 显式重定向,别依赖邮件
0 3 * * * /home/razer/bin/backup.sh >> /var/log/backup.log 2>&1

# 2. 不想收邮件就明确关掉
MAILTO=""

还有一层更隐蔽的问题:当 stdout 是文件而不是终端时,C 库的 stdio 从行缓冲切换成块缓冲(通常 4KB 或 8KB)。脚本跑了一半崩掉,缓冲区里那几行日志跟着一起没了,于是你看到的现象是“日志停在半句话上”。排查这类问题时:

stdbuf -oL -eL /home/razer/bin/backup.sh >> /var/log/backup.log 2>&1

Python 用 python3 -u 或设 PYTHONUNBUFFERED=1。另外记得在脚本里写 set -euo pipefail,否则中间某条命令失败,脚本照样以 0 退出,你连失败都发现不了。

一张对照表

现象 首先怀疑 验证方式
完全没执行 crontab 归属、/etc/cron.d 权限、服务未启动 grep CRON /var/log/syslog
command not found PATH 缺目录 临时任务跑 /usr/bin/env,把结果写到文件
解释器版本不对 shell 配置未加载 脚本里打 which python3、python3 -V
文件找不到 工作目录是 $HOME 脚本里打 pwd
参数被截断 % 未转义 把命令包进脚本再跑
报错看不到 无 MTA、输出未重定向 加 >> log 2>&1
日志缺最后几行 块缓冲 用 stdbuf -oL 或 -u

最省事的定位办法,是在 crontab 里临时加一条探测任务,等一分钟:

* * * * * /usr/bin/env > /tmp/cron-env.txt 2>&1

然后 cat /tmp/cron-env.txt,和你在终端里 env | sort 的结果对比,差异一目了然。查完记得删掉。

比 cron 更省心的选择

如果任务开始变多,或者需要“错过就补跑”“失败要告警”,systemd timer 值得换过去。环境是显式声明的,日志自动进 journal,不需要再和缓冲、PATH、MTA 斗智斗勇。

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
User=razer
WorkingDirectory=/srv/app
Environment=PATH=/home/razer/.local/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/srv/app/scripts/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now backup.timer
systemctl list-timers --all
journalctl -u backup.service -n 50

Persistent=true 会在关机期间错过的任务下次开机后补跑,RandomizedDelaySec 则能避免一堆机器在同一秒涌向同一台服务器。

回到开头那个空备份目录:先看日志确认 cron 有没有调用,再看 PATH 和 shell 配置,最后把工作目录和输出固定下来。这四步走完,绝大多数“手动能跑、cron 不跑”的问题都会现出原形。

相关文章
|
9天前
|
JavaScript API iOS开发
DeepSeek Harness下载、安装、配置一篇搞定(2026整理,送常用插件)
DeepSeek Harness(DSH)是DeepSeek于2026年开源的MIT协议AI Agent框架,支持插件化组装模型、工具、沙箱与UI,兼容主流大模型。本文提供免Node.js的桌面端安装包(Win/macOS),含详细图文教程及热门插件推荐。
3789 38
|
8天前
|
Linux 开发工具 iOS开发
Codex保姆级教程(下载+安装+登录+使用,含所有版本,2026最新)
Codex是OpenAI推出的开源AI编程智能体,本地运行,可理解整个代码库,支持自然语言指令改代码、执行命令、跑测试、修Bug,兼容Win/macOS/Linux。(239字)
3428 12
|
4天前
|
云安全 人工智能 安全
|
9天前
|
iOS开发 Docker MacOS
【2026实测】Docker Desktop下载、安装、汉化、配置教程(附官网安装包)
Docker Desktop 是 Docker 官方推出的桌面容器管理工具,集成引擎、CLI、图形界面与 Kubernetes,支持 Win/macOS/Linux。2026 最新版 4.88.1 强化 ARM 原生运行、镜像加速及资源可视化,附中文汉化与国内镜像配置指南。
1735 6
|
10天前
|
安全 算法 定位技术
《高性价比人生指南》完整原版:普通人用最少的资源,过好这一生的底层逻辑~How To Live Better
📖《高性价比人生指南》是一份基于实证、拒绝焦虑的生存手册,倡导以“人生时薪”为视角,重新计算时间、金钱与精力的成本与收益。它不承诺逆袭,只教人算清账:省下的钱若耗掉时间便亏;买体验比买物品更值;睡眠是最高杠杆的健康投资;关系贵在情绪价值而非表面热络;学习重在改变行为而非自我安慰。核心理念:用低成本、高回报的微小行动,换取长期的选择权——不是成为谁,而是掌控自己人生的主动权。
2049 4
|
云安全 安全 网络安全
|
3天前
|
Linux 开发工具 iOS开发
2026最新codex下载安装保姆级使用教程(下载+安装+登录+使用,含所有历史版本)
本文详解OpenAI Codex国内快速上手指南:提供Win/macOS/Linux全平台免安装包、npm/Homebrew等多方式安装、账号/API Key双登录、简体中文切换,覆盖下载、安装、汉化、使用及高频避坑(如微软商店失败、npm超时等),10分钟即可跑通。
607 0
|
22天前
|
人工智能 API 开发工具
DeepSeek Harness 神插件:白嫖 13 家大厂模型
`dsh-our-free-model` 是 DeepSeek Harness 神级插件:零配置、零API Key、零账号,聚合13家大厂免费模型(含DeepSeek V4.1 Flash、Kimi K3等),支持本地网关、用量看板与自动降级,真正实现“白嫖即用”。
3972 55