Administrator
Administrator
发布于 2026-10-29 / 0 阅读
0
0

Shell 脚本手动成功,定时执行却失败:从路径、环境到日志排查

适合读者:能写简单 Bash 命令,准备把日常工作交给定时任务的初级运维人员。示例使用 Bash 和常见 cron 实现,全部文件放在独立练习目录。

定时任务排查路线:触发、用户、环境、脚本、结果

1. “手动成功”没有验证哪些条件

你在终端里执行统计脚本,报告正常生成;放进 crontab 后却没有报告。先把问题分成两段:cron 有没有发起任务,任务发起后有没有完成。

人工执行时,终端已经带上了当前目录、PATH、环境变量、登录用户和权限。定时任务只拥有它自己的执行环境。cron 通常不会替你进入项目目录,也不会自动加载终端里激活的 Python 虚拟环境。

cron 的默认 Shell、用户环境及命令字段处理方式,可对照 Cronie 的 crontab 手册。不同发行版采用的实现可能不同,应结合本机 man 5 crontab 核对。

2. 从哪里确认任务有没有被调度

先看自己的任务表,再确认时间:

crontab -l
whoami
date
timedatectl

用户 crontab 里的任务按该用户运行。以 root 查看到的任务表,和普通用户查看到的任务表可能完全不同。

常见服务名是 Ubuntu、Debian 的 cron.service,或 RHEL 系发行版的 crond.service。按本机实际服务选择一组查看:

systemctl status cron.service --no-pager
sudo journalctl -u cron.service --since '30 minutes ago' --no-pager
systemctl status crond.service --no-pager
sudo journalctl -u crond.service --since '30 minutes ago' --no-pager

有的系统把执行记录交给 syslog,日志位置和内容也会不同。调度日志里出现 CMD,只能说明 cron 尝试执行了命令,不表示脚本已经完成。

3. 五个时间字段别写错

用户 crontab 的一行由五个时间字段和一段命令组成:

分 时 日 月 周  命令
*/5 * * * *   每逢分钟数能被 5 整除时执行
30 2 * * *    每天 02:30 执行
0 9 * * 1-5   周一到周五 09:00 执行

时间通常按 cron 使用的时区解释,不一定和浏览器或自己的电脑一致。*/5 是日历时间的分钟匹配,并不是从保存任务表那一刻开始精确等待五分钟。

crontab -e 编辑的用户任务行没有“用户名”字段;/etc/crontab、/etc/cron.d/ 的系统任务则有用户名字段。混用格式会让任务无法正确执行。

4. 实验:一个最容易失败的相对路径

先创建练习目录:

mkdir -p "$HOME/.local/share/ops-cron-lab/data"
mkdir -p "$HOME/.local/share/ops-cron-lab/logs"

使用编辑器保存 ~/.local/share/ops-cron-lab/data/users.csv:

id,name
1,Alice
2,Bob

保存一个故意依赖当前目录的 ~/.local/share/ops-cron-lab/bad-report.sh:

#!/bin/bash
wc -l data/users.csv

从项目目录执行:

cd "$HOME/.local/share/ops-cron-lab"
/bin/bash ./bad-report.sh

这里可以读到文件。换个目录再执行同一个脚本:

cd /tmp
/bin/bash "$HOME/.local/share/ops-cron-lab/bad-report.sh"

预期出现“找不到 data/users.csv”。脚本的绝对路径正确,并不意味着脚本内部的相对路径也会自动跟着改变。wc -l 还把表头算进去了,因此这个版本连统计含义也需要调整。

5. 改成能独立执行、能报告失败的脚本

把下面内容保存为 ~/.local/share/ops-cron-lab/report.sh:

#!/bin/bash
set -u
set -o pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) || exit 1
INPUT_FILE="$SCRIPT_DIR/data/users.csv"
OUTPUT_DIR="$SCRIPT_DIR/output"
REPORT_FILE="$OUTPUT_DIR/report.txt"

printf '[%s] start user=%s cwd=%s\n' \
  "$(date -Is)" "$(id -un)" "$PWD"

if [[ ! -r "$INPUT_FILE" ]]; then
  printf 'ERROR: input is not readable: %s\n' "$INPUT_FILE" >&2
  exit 2
fi

if ! mkdir -p -- "$OUTPUT_DIR"; then
  printf 'ERROR: cannot create output directory\n' >&2
  exit 3
fi

if ! awk 'NR > 1 && NF {count++} END {printf "users=%d\n", count}' \
  "$INPUT_FILE" > "$REPORT_FILE"; then
  printf 'ERROR: report generation failed\n' >&2
  exit 4
fi

printf '[%s] success report=%s\n' "$(date -Is)" "$REPORT_FILE"

这里有四个变化:根据脚本自身位置找输入文件;对变量展开使用双引号;关键步骤失败时明确返回非零退出码;输出包含时间、用户和结果。

这个统计只处理示例中“一行一个用户、首行是表头”的简单 CSV,不是完整 CSV 解析器。数据含引号内换行时应换用支持 CSV 格式的工具。

set -u 能发现未定义变量,pipefail 能改变管道的失败判断;它们不能替代具体错误处理。尤其不能把 set -e 当成任何场景都可靠的异常捕获机制。管道退出状态的定义见 Bash Pipelines 手册。

6. 先用简化环境验证,再交给 cron

从任意目录运行:

cd /tmp
env -i PATH=/usr/bin:/bin /bin/bash \
  "$HOME/.local/share/ops-cron-lab/report.sh"
printf 'exit_code=%s\n' "$?"
cat "$HOME/.local/share/ops-cron-lab/output/report.txt"

预期退出码为 0,报告内容为 users=2。这个测试去掉了大部分继承环境,可以暴露路径和环境依赖,但并没有模拟 cron 的全部条件,例如调度时间、PAM、SELinux 或运行用户差异。

如果脚本依赖虚拟环境中的 Python,直接调用该环境的绝对路径,例如 /opt/my-app/.venv/bin/python;不要指望 cron 会执行终端里曾经运行过的 activate。

7. 添加任务,并把错误输出也留下

先确认前面已经创建 logs 目录,再运行 crontab -e,添加以下内容。保留已有任务:

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
*/5 * * * * /bin/bash "$HOME/.local/share/ops-cron-lab/report.sh" >> "$HOME/.local/share/ops-cron-lab/logs/cron.log" 2>&1

任务行中的 $HOME 由执行命令的 Shell 展开,指向任务所属用户的家目录。脚本通过 /bin/bash 明确选择解释器。

>> 追加标准输出,2>&1 让错误输出进入同一处。顺序有意义,详见 Bash 重定向手册。

一个容易漏掉的细节是:Shell 会先打开日志文件,再启动脚本。 如果日志目录不存在或不可写,脚本可能根本没被执行,因此不能指望脚本内部再去创建这个外层日志目录。

等到下一个匹配时间后检查:

tail -n 30 "$HOME/.local/share/ops-cron-lab/logs/cron.log"
stat "$HOME/.local/share/ops-cron-lab/output/report.txt"

验证应同时包含调度记录、脚本成功日志、报告内容和修改时间。单凭日志文件存在,无法证明最新一次任务执行成功。

8. 三个进阶细节,遇到时知道怎么查

百分号。 在常见 cron 实现的命令字段里,未转义的 % 有特殊含义。日期格式化等复杂操作放进脚本,更容易阅读和验证。

任务重叠。 如果任务每五分钟触发,但一次要运行八分钟,便可能同时执行。需要互斥时可使用本机 flock,先理解“拿不到锁就跳过”是否符合业务要求;锁也要放在该用户可写的位置。

日志增长。 >> 会持续追加。长期任务应配套日志轮转或统一日志收集,并监控最近成功时间。成功日志里不要打印令牌、密码和完整敏感环境变量。

9. 遇到问题时按这个顺序回答

问题 证据
时间是否匹配? 任务表达式、服务器时间和时区
是否触发? cron 服务状态和调度日志
谁在运行? 任务表所属用户、脚本记录的用户
环境是否足够? PATH、解释器路径、工作目录
哪一步失败? 错误输出、明确的退出码
结果是否正确? 业务文件、内容、修改时间

练习结束后,用 crontab -e 删除自己添加的那一行即可;不要使用会清空整个任务表的方式清理实验。下次遇到“定时任务没生效”,先区分触发失败和执行失败,就能少走很多弯路。


评论