cron任务不运行的5个常见原因及排查方法
cron任务从未运行,常见原因包括PATH过短、百分号未转义、编辑了错误的crontab、输出被发往邮件,以及脚本依赖登录Shell。
为什么您的 cron 任务没有运行
一个“从未运行”的 cron 任务几乎总是运行过。它是在不同于您 shell 的环境中运行的,可能在第一秒就失败了,而且消息被发送到了您没有查看的位置。以下 5 个原因几乎可以解释所有此类报告:搜索路径、百分号、错误的 crontab 文件、输出被发送到邮件,以及脚本依赖登录会话。
cron 是一个 daemon(后台服务)。它读取 crontab 文件,并按计划启动命令。它不会读取您的 .bashrc,不会打开终端,不会启动登录 shell,也不会在命令失败时通知您。下面的每个原因都源于这 4 个事实。
请按顺序排查,并先回答贯穿所有问题的核心问题:cron 是否确实触发了任务?“cron 从未启动任务”和“任务已启动后退出”是两个完全不同的问题,彼此没有共同原因,因此应先确认这一点。
cron 是否实际触发?
不同发行版系列使用不同的 daemon 单元名称。请分别检查这两个名称,然后读取日志。
systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"Debian 和 Ubuntu 将该单元称为 cron。Fedora、Rocky 和 Alma 将其称为 crond。一台机器上只会存在其中一个名称,因此两个命令中有一个报告单元未知是正常现象,不表示存在故障。
读取系统实际写入的条目。不要去查找指南中复制的某一行,因为不同 cron 实现和日志配置的措辞可能不同。这里只检查两点:在计划指定的分钟是否存在条目,以及该条目是否包含您的命令。包含您的命令的条目表示 cron 已完成自身工作,故障位于命令内部。完全没有条目表示 cron 从未加载您的计划,这对应下面的原因 3。
某些镜像会通过 rsyslog 将 cron 消息写入文件,而不是写入 journal。请在 /var/log 中查找以 cron 或 syslog 命名的文件,然后读取文件末尾内容。
ls -l /var/log
sudo tail -n 50 /var/log/syslog如果单元和日志都不存在,可能只是未安装 cron。精简的云镜像和容器通常不会预装它。
dpkg -l cron
rpm -q cronie
sudo apt install cron
sudo dnf install cronie
sudo systemctl enable --now cron原因 1:cron 没有您的 PATH
交互式 shell 会根据 /etc/profile、~/.profile、~/.bashrc 以及这些文件所加载的全部内容构建 PATH。cron 作业不会执行这些内容。cron 会使用自己的精简环境启动命令,因此找不到位于标准系统目录之外的程序。/usr/local/bin、/opt 下的程序、语言版本管理器、Python 虚拟环境或 Go 工作区中的程序都可能受此影响。作业会在第一行失败,shell 会写入类似 “not found” 的错误。具体措辞取决于执行该命令的 shell。
找出作业使用的每个命令的实际路径。
command -v docker
command -v node
readlink -f "$(command -v node)"然后将这些绝对路径写入作业,或在 crontab 顶部设置一次 PATH。
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * /usr/local/bin/mytool run在您自己的计算机上使用 echo "$PATH" 获取该列表,并删除仅在交互式会话中存在的内容。这里有一条重要规则:cron 不会展开这些赋值行中的变量。PATH=$PATH:/usr/local/bin 会保存字面文本 $PATH:/usr/local/bin,因此作业最终得到的搜索路径中没有任何可用目录。请写出完整列表。
版本管理器需要的不只是路径。nvm、pyenv、rbenv 和 asdf 会根据您的 .bashrc 安装 shell 函数或 shims 目录,而 cron 作业不会读取该文件。请使用绝对路径调用对应版本的二进制文件,或在您自己的脚本中将版本管理器的初始化脚本作为第一行加载。
原因 2:百分号会结束命令
在 crontab 的命令字段中,% 不是普通字符。第一个未转义的 % 会结束命令。其后的所有内容都会作为标准输入传递给命令,后续每个 % 都会转换为换行符。cron 确实支持使用此功能向程序提供简短输入;这也是带日期文件名的命令成为 crontab 中经典错误条目的原因。
编写 0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +%F).tar.gz /srv/site 后,tar 永远看不到格式化后的日期。cron 会在第一个 % 处截断该行,因此 shell 收到的是未完成的命令替换,其余内容则作为标准输入传入。必须使用反斜杠转义每个百分号。
0 3 * * * /usr/bin/tar -czf /srv/backups/site-$(date +\%F).tar.gz /srv/site这一行会按顺序由两层处理。\% 是 cron 规则,由 cron 在启动任何内容之前应用。$(date +\%F) 是命令替换,由 cron 启动的 shell 在之后应用。关键在于明确每个字符由哪一层处理。
更稳妥的做法是完全不要将逻辑写入 crontab。将逻辑放入脚本中,因为在脚本里百分号没有特殊含义。
#!/bin/bash
set -euo pipefail
stamp="$(date +%F)"
tar -czf "/srv/backups/site-${stamp}.tar.gz" /srv/site这样,crontab 行中只需包含路径和重定向,不包含其他内容。能够一眼读懂的 crontab 才便于调试。
原因 3:您编辑的是哪个 crontab?
crontab 并不只有一个。系统中有多个文件,所有者和字段数量各不相同。将任务写入错误的文件后,任务就不会生效。
crontab -e编辑执行该命令的用户的 crontab。sudo crontab -e编辑 root 的 crontab。两个人排查同一台服务器时,经常会读取两个不同的文件。sudo crontab -l -u deploy列出其他用户的 crontab。您可以用它确认应运行该任务的账户实际安装了哪些任务。/etc/crontab以及/etc/cron.d中的每个文件,都会在调度字段和命令之间多出一个字段:要使用的用户。将包含 5 个字段的用户 crontab 行粘贴到/etc/cron.d后,命令的第一个单词会被当作用户名解析。/etc/cron.d中的文件名只能包含字母、数字、下划线和连字符。名为backup.sh或site.conf的文件会仅因文件名不符合要求而被跳过。将其重命名为backup,然后再次检查日志。/etc/cron.d中的文件应归 root 所有,并且组用户和其他用户不得拥有写权限。ls -l /etc/cron.d可同时显示这两项信息。- 放入
/etc/cron.daily及其同级目录的脚本也必须遵循相同的命名规则,并且必须设置执行权限。缺少执行权限会导致脚本被静默跳过。 /etc/cron.allow和/etc/cron.deny决定哪些用户可以安装 crontab。如果您的服务器上存在其中任意一个文件,请先读取它,再假定当前用户可以安装 crontab。
请使用 crontab 命令安装用户 crontab,不要手动编辑 spool 文件,因为 crontab 会在安装前解析该文件。保存后,请查看命令返回的输出。如果命令拒绝该文件,之前的版本仍会继续生效,您的修改也不会生效。这种情况看起来就像 cron 忽略了您的任务。
所有者也会决定权限。root 的 crontab 中的任务会创建归 root 所有的文件,读取这些文件的应用可能无法写入。普通用户 crontab 中的任务无法读取仅 root 可访问的目录。应让所有者与任务匹配:应用维护任务应由应用自身的账户执行,这也是 用系统 cron 任务替换 WordPress wp-cron 的原因。任务创建的文件的模式来自它继承的 umask,而该值可能与 shell 中的 umask 不同。因此,如果任务的输出文件无法读取,建议阅读 umask 如何设置文件权限。
原因 4:输出被发送到了没人读取的邮件
cron 会收集任务写入标准输出和标准错误的所有内容。如果任务产生了任何输出,cron 就会将这些文本交给本地邮件系统,收件人是 crontab 的所有者,或 MAILTO 所指定的地址。在精简版 VPS 上通常没有安装 MTA(邮件传输代理),因此没有组件负责投递这些邮件。错误信息只存在了片刻,随后就无处可查。这就是故障任务看起来没有任何输出的根本原因。
请将输出发送到您可以控制的文件。
0 3 * * * /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1>> 会将标准输出追加到文件中。2>&1 会将标准错误指向当前标准输出所指向的位置,因此必须放在该重定向之后。如果顺序写反,例如 2>&1 >> file,标准错误仍会保留原来的目标,而您要查找的错误恰好不会写入文件。
journal 也是一个合适的目标。logger 会使用您指定的标签写入 syslog。
0 3 * * * /usr/local/sbin/backup-site.sh 2>&1 | logger -t backup-site使用 journalctl -t backup-site 读取这些内容。这样,任务自身的输出会与 cron 条目保存在一起,便于按时间线排查。如果还需要记录哪位用户在服务器上执行了哪些命令,这是另一套系统;审计服务器上的用户命令对此有详细说明。
在 crontab 顶部添加 MAILTO="",可以关闭其下任务的邮件通知。将 MAILTO 设置为真实地址,只有在系统存在可正常工作的 MTA 时才有用。因此,在依赖邮件通知之前,请先确认邮件确实能够离开服务器。
调试时请遵循一条规则:永远不要追加 > /dev/null 2>&1。它是每个 crontab 中最常见的一行,却会丢弃您唯一拥有的证据。任务正常运行后,如果仍有需要,再将它加回去。
原因 5:脚本假定环境会提供 cron 不会提供的内容
找到命令并捕获其输出后,剩下的问题都与当前会话免费提供的环境有关。
- Shell 可能不是 bash。使用
ls -l /bin/sh检查。在 Debian 和 Ubuntu 上,它指向 dash,因此双中括号测试、数组和source会因语法错误而失败。为脚本添加#!/bin/bash行并调用该脚本,或在 crontab 顶部设置SHELL。 - 工作目录不是您手动执行命令时所在的目录。始终使用绝对路径,或在脚本第一行使用
cd切换到目标目录。相对路径是任务“手动运行时正常”的最常见单一原因。 - Locale 不是当前会话使用的 Locale。任何格式化日期或数字、或对文本排序的操作,在不同的
LANG下都可能产生不同输出。如果后续步骤会解析该输出,应在脚本中设置 Locale,不要依赖默认环境。 - 没有 TTY(终端)。需要确认、打开编辑器或绘制进度条的命令可能会挂起或退出。添加工具提供的非交互式选项。
- 没有 SSH agent。cron 环境中没有
SSH_AUTH_SOCK,因此原本因 agent 已加载而能正常工作的ssh或rsync命令现在会认证失败。为任务配置专用密钥,并确保该密钥归任务运行用户所有。 - 没有用户会话总线,因此 cron 任务中的
systemctl --user会失败,直到设置XDG_RUNTIME_DIR。更好的解决方案是使用 systemd system unit。
在 Fedora、Rocky 和 Alma 上,还需检查另一个问题。SELinux 会限制 cron 任务,因此即使文件权限正确,任务访问带有异常标签的路径时仍会被拒绝。使用 sudo ausearch -m avc -ts recent 检查拒绝记录,并先阅读 服务器的 SELinux 基础知识,再考虑关闭任何安全机制。
展示 cron 环境的 1 分钟探针
不要再猜 cron 的环境包含什么,直接读取它。编写一个转储全部环境的脚本,每分钟执行一次,等待片刻,然后读取输出文件。
cat > /home/deploy/cron-probe.sh <<'EOF'
#!/bin/bash
echo "=== probe ==="
date -Is
pwd
id
echo "SHELL=$SHELL"
echo "LANG=$LANG"
command -v node || echo "node is not on this PATH"
env | sort
EOF
chmod +x /home/deploy/cron-probe.sh在实际任务运行所使用的用户的 crontab 中添加一行,并在两侧都使用绝对路径。
* * * * * /home/deploy/cron-probe.sh >> /home/deploy/cron-probe.log 2>&1等待 1 分钟,然后读取 /home/deploy/cron-probe.log,并将其与您在自己的 shell 中执行的相同命令进行比较。PATH 行、工作目录和 locale 通常就足以解释故障。请注意设置中的两个细节:百分号位于脚本内部,不受 cron 规则影响;日志路径可由任务用户写入。
找到答案后立即删除该 crontab 行。每分钟运行一次并向文件追加内容的任务会填满小容量磁盘,而且整个过程不会发出明显提示。
计划是否符合您的预期?
用户 crontab 行以 5 个字段开头:分钟、小时、月中的日期、月份、星期几。其中两个字段的交互方式常常令人意外。
当月中的日期和星期几都受到限制时,也就是说两者都不是 *,cron 会在任一字段匹配时运行任务。0 0 13 * 5并不表示“13 号星期五”。它会在每月 13 日的午夜运行,也会在每个星期五的午夜运行。若要指定某一天,请将这两个字段中的一个保留为 *,再在脚本中检查另一个字段。
cron 使用系统时区。许多 VPS 镜像默认使用 UTC(协调世界时),因此您安排在 03:00 运行的任务会在 03:00 UTC 运行,这可能正是您当地下午的某个时间。timedatectl会显示您的服务器实际使用的时区。请查看实际值,不要假定它与您的笔记本电脑一致。
还需要注意另外两个计划任务陷阱。@reboot会在 cron 自身启动时触发,这并不等同于网络已准备就绪。因此,需要 DNS 或远程主机的任务可能会在启动时失败,但之后每次手动运行都能成功。此外,如果任务运行缓慢,前一个副本尚未结束时,新的副本仍可能再次启动。请使用锁将其包装起来。
*/5 * * * * /usr/bin/flock -n /tmp/backup-site.lock /usr/local/sbin/backup-site.sh >> /var/log/backup-site.log 2>&1当锁已被占用时,flock -n会立即退出,因此重叠运行会停止,不会与第一个实例同时累积运行。
systemd timer 更适合的场景
cron 只擅长一件事:在指定时间运行命令。在其他方面,它都不够完善。timer 无需重定向即可将输出写入 journal,之后可以查询退出状态,还能与 network-online.target 配置启动顺序,并设置随机延迟,避免数百台服务器在同一秒启动任务。当任务需要其中任何一项功能时,在 VPS 上配置systemd 服务和 timer,比维护一行 crontab 更省事。重试行为也应由它负责,因为systemd 重启策略决定失败后如何处理,而 cron 完全无法回答这个问题。
小型任务继续使用 cron。需要依赖项或重试策略的任务,改用 timer。两者可以在同一台服务器上同时运行,因此不必一次完成整个迁移。
FAQ
我的 cron 作业手动运行正常,但通过 cron 运行失败,为什么?
因为您的 shell 环境与 cron 的环境不同。登录 shell 会读取 /etc/profile 和 ~/.bashrc,这些文件会设置 PATH、区域设置和代理变量。cron 启动命令时不会继承这些设置,工作目录也不同,有时使用的 shell 也不同。为每个命令使用绝对路径,在 crontab 顶部或脚本中设置所需变量,并安排一个每分钟运行的探测作业,将 env | sort、pwd 和 id 的输出写入日志文件。这样您可以读取 cron 的实际环境,而不是猜测环境配置。
如何确认 cron 确实运行了我的作业?
读取 daemon 的日志。在 Debian 和 Ubuntu 上使用 journalctl -u cron,在 Fedora、Rocky 和 Alma 上使用 journalctl -u crond;某些镜像会通过 rsyslog 将消息写入 /var/log 下的文件。查找计划指定的分钟对应的日志条目,并确认其中包含您的命令。没有条目表示 cron 从未获取到该计划,因此请确认您编辑的是正确的 crontab。存在条目但没有结果,表示命令已启动后退出,因此请通过重定向捕获其输出。
为什么 date +%Y 在 crontab 中会导致问题?
cron 会将命令字段中的 % 视为特殊字符。第一个未转义的 % 会结束命令,后面的所有内容都会作为标准输入传递给该命令,之后的每个 % 都会变成换行符。因此,使用日期格式生成的文件名不会按预期传递给程序。请将每个百分号转义为 \%,或者将命令移入脚本,再从 cron 调用该脚本;在脚本中,百分号没有特殊含义。
cron 作业的输出会写到哪里?
输出会发送到本地邮件系统,收件人是 crontab 的所有者,或由 MAILTO 指定的地址。大多数 VPS 镜像未安装邮件传输代理,因此消息会被丢弃,作业看起来就像没有输出。使用 >> /path/to/log 2>&1 将输出重定向到文件,并保持该顺序,使标准错误跟随标准输出;也可以通过 logger -t myjob 传递输出,再使用 journalctl -t myjob 读取。仍在调试时,不要使用 > /dev/null 2>&1。
应该使用 cron 还是 systemd timer?
对于固定时间运行的简单命令,请使用 cron,尤其是您可能需要将作业迁移到不运行 systemd 的服务器时。如果您希望无需重定向即可将输出写入 journal、查询退出状态、在网络启动后按顺序运行、随机延迟启动,或在失败后按策略重试,请使用 timer。两者可以在同一台服务器上运行,因此可以在作业需要这些功能时逐个迁移。