适合读者:能写简单 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 删除自己添加的那一行即可;不要使用会清空整个任务表的方式清理实验。下次遇到“定时任务没生效”,先区分触发失败和执行失败,就能少走很多弯路。