SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-09-04

Cron任务不运行?排查5个常见原因

Cron任务看似从未运行,通常是环境不同导致立即失败。本文排查精简PATH、未转义百分号、错误crontab、邮件输出和依赖登录Shell。

为什么您的 cron 任务没有运行

所谓“从不运行”的 cron 任务几乎总是实际运行过。它在不同于您 shell 的环境中运行,可能在第一秒就失败了,而输出被发送到了您没有查看的位置。几乎所有此类问题都可归因于以下 5 个原因:搜索路径、百分号、错误的 crontab 文件、输出被发送到邮件,以及脚本依赖登录会话。

cron 是一个守护进程(后台服务),负责读取 crontab 文件并按计划启动命令。它不会读取您的 .bashrc,不会打开终端,不会启动登录 shell,也不会在命令失败时通知您。下面的每个原因都源于这 4 个事实。

请按顺序排查,并先回答贯穿所有问题的核心问题:cron 是否真的触发了任务?“cron 从未启动任务”和“任务已启动但退出”是两个完全不同的问题,二者没有共同原因,因此应先确认这一点。

cron 是否实际触发?

不同发行版系列使用的 daemon unit 名称不同。请检查两者,然后读取日志。

systemctl status cron
systemctl status crond
journalctl -u cron --since "2 hours ago"
journalctl -u crond --since "2 hours ago"

Debian 和 Ubuntu 将该 unit 称为 cron。Fedora、Rocky 和 Alma 将其称为 crond。在同一台计算机上只会存在其中一个名称,因此两条命令中有一条报告 unknown unit 是正常现象,不表示存在故障。

读取您自己的系统写入的日志条目。不要查找从指南中复制的某一行,因为不同 cron 实现和日志配置的措辞可能不同。这里只检查两点:计划指定的分钟是否有对应条目,以及该条目是否包含您的命令。包含您的命令的条目表示 cron 已完成自身工作,故障出在命令内部。完全没有条目表示 cron 从未获取到您的计划,这对应下面的 cause 3。

某些镜像通过 rsyslog 将 cron 消息写入文件,而不是写入 journal。请在 /var/log 中查找以 cron 或 syslog 命名的文件,然后读取文件末尾内容。

ls -l /var/log
sudo tail -n 50 /var/log/syslog

如果 unit 和日志都不存在,可能只是未安装 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 函数或 shim 目录,而 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 并不只有一个。系统中有多个文件,所有者和字段数量各不相同。将任务写入错误的文件后,cron 不会看到它。

  • 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.shsite.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 可访问的目录。应让所有者与任务匹配:应用维护任务应由应用自己的账户执行。这也是 将 WordPress wp-cron 替换为系统 cron 任务 的依据。任务创建文件的模式取决于它继承的 umask,而该值与您 shell 中的值可能不同。如果任务输出的文件无法读取,建议阅读 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 与您的会话不同。任何格式化日期或数字、或对文本排序的操作,都可能在不同的 LANG 下产生不同输出。如果后续步骤会解析该输出,请在脚本中设置 locale,不要依赖默认环境。
  • 没有 TTY(终端)。需要确认、打开编辑器或绘制进度条的命令可能会挂起或退出。添加该工具提供的非交互式选项。
  • 没有 SSH agent。SSH_AUTH_SOCK 不在 cron 的环境中,因此某个曾因 agent 已加载而成功的 sshrsync 命令现在会认证失败。为任务单独提供一个密钥,并将其所有者设为任务用户。
  • 没有用户会话总线,因此 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 个字段开头:分钟、小时、月份中的日期、月份、星期几。其中 2 个字段的交互方式经常令人意外。

当月份中的日期和星期几都受到限制时,即两者都不是 *,cron 会在任一字段匹配时运行任务。0 0 13 * 5 并不表示“星期五的 13 号”。它会在每个月 13 号的午夜运行,也会在每个星期五的午夜运行。若要指定某一天,请将这两个字段中的一个设为 *,然后在脚本中判断另一个字段。

cron 使用系统时区。许多 VPS 镜像默认设置为 UTC(协调世界时),因此您安排在 03:00 运行的任务会在 03:00 UTC 运行,这可能正是您当地下午的某个时间。timedatectl 会显示您的服务器实际使用的时区。请读取服务器的时区设置,不要假定它与笔记本电脑一致。

另外还有 2 个值得注意的计划陷阱。@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 更省事。编写服务单元时,还需要回答一个 cron 从未要求回答的问题:该单元如何确认任务确实已经启动。因此,请先阅读 Type= 对 simple、forking 和 notify 单元的含义。如果脚本在默认类型下自行变为守护进程,单元会显示为 active,但实际没有运行中的进程。重试行为也应在这里配置,因为 systemd 重启策略决定失败后如何处理,而 cron 完全没有对应机制。

简单任务继续使用 cron。包含依赖关系或需要重试策略的任务,改用 timer。两者可以在同一台服务器上共存,因此不必一次完成迁移。

FAQ

为什么我的 cron 作业手动运行正常,但通过 cron 运行失败?

因为您的 shell 环境与 cron 的环境不同。您的登录 shell 会读取 /etc/profile~/.bashrc,这些文件会设置 PATH、locale 和 agent 变量。cron 启动命令时不会加载这些内容,工作目录也不同,有时使用的 shell 也不同。为每个命令使用绝对路径,在 crontab 顶部或脚本中设置所需变量,并安排一个每分钟运行的探测作业,将 env | sortpwdid 的输出写入日志文件。这样您可以读取 cron 的实际环境,而不是猜测其配置。

如何检查 cron 是否确实运行了我的作业?

读取 daemon 的日志。在 Debian 和 Ubuntu 上使用 journalctl -u cron;在 Fedora、Rocky 和 Alma 上使用 journalctl -u crond。某些镜像会通过 rsyslog 将消息写入 /var/log 下的文件。查找调度时间对应分钟的日志条目,并确认其中包含您的命令。没有条目表示 cron 从未加载该调度,因此请确认您编辑的是正确的 crontab。有条目但没有结果表示命令已启动后退出,因此请通过重定向捕获其输出。

为什么 date +%Y 在 crontab 中会失效?

cron 会将命令字段中的 % 视为特殊字符。第一个未转义的 % 会结束命令,后面的所有内容都会作为标准输入传递给该命令,之后每个 % 都会变成换行符。因此,按日期格式生成的文件名不会传递给您编写该文件名时 intended 的程序。将每个百分号转义为 \%,或者将命令移入脚本,再从 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。两者可以在同一台服务器上运行,因此可以在作业需要这些功能时逐个迁移。