一、先确认它到底跑没跑
crontab -l # 当前用户的定时任务
grep CRON /var/log/syslog # Debian/Ubuntu
grep CRON /var/log/cron # CentOS/RHEL
systemctl status cron # 服务是否在运行
日志里出现 (root) CMD (xxx) 说明触发了,问题在脚本内部;完全没有记录,说明 cron 服务没跑或时间表达式不匹配。
二、六个经典原因
1. 环境变量缺失
cron 不加载 .bashrc、.profile,PATH 通常只有 /usr/bin:/bin。于是 python3、node、docker 全成了「command not found」。
解决:在 crontab 顶部显式声明,或用绝对路径。
PATH=/usr/local/bin:/usr/bin:/bin
LANG=zh_CN.UTF-8
SHELL=/bin/bash
2. 相对路径
cron 的工作目录是用户家目录,不是脚本所在目录。脚本里写 ./data.json 会找不到。要么在脚本里 cd $(dirname $0),要么在 crontab 里先 cd。
3. % 需要转义
crontab 里 % 是特殊字符(表示换行),date +%F 必须写成 date +\%F。这也是「命令里带日期就失效」的原因。
4. 脚本没有执行权限
./backup.sh 需要 chmod +x,或者直接用 bash /path/backup.sh 调用(更稳,不依赖 shebang 和权限)。
5. 时区不对
服务器是 UTC,你按北京时间写了 0 2 * * *,实际是北京时间 10 点执行。检查 timedatectl,或在 crontab 顶部加 CRON_TZ=Asia/Shanghai(部分实现支持),最稳的是把服务器时区设为 Asia/Shanghai。
6. 上一次还没跑完
任务耗时超过间隔,多个实例叠在一起抢同一份文件。用 flock 加锁:
*/5 * * * * flock -n /tmp/backup.lock /bin/bash /opt/backup.sh >> /var/log/backup.log 2>&1
-n 表示拿不到锁就跳过本次,不排队。
三、一定要重定向输出
cron 会把脚本的 stdout/stderr 通过邮件发给用户,服务器没配邮件就全部丢弃。所以出错时什么都看不到:
0 2 * * * /bin/bash /opt/backup.sh >> /var/log/backup.log 2>&1
>> 追加、2>&1 把错误也写进去。长期运行要配 logrotate,否则日志会撑满磁盘。
四、时间表达式回顾
0 2 * * * 每天 2:00
*/10 * * * * 每 10 分钟
0 9-18 * * 1-5 工作日 9–18 点整点
0 3 1 * * 每月 1 日 3:00
五个字段顺序是「分 时 日 月 周」。写完用 Cron 解析工具 看中文释义和未来 5 次执行时间,比脑推可靠。推算出来的时间点还可以用 时间戳转换 核对时区。
五、更现代的选择
- systemd timer:日志统一进 journal,支持
Persistent=true(关机错过的任务开机补跑),依赖管理更好。 - 应用内调度:需要秒级、需要分布式锁时,用专业调度器(如 xxl-job、Celery beat)。
crontab 适合「简单、单机、不需要补跑」的场景,够用但排错信息少。
六、排查清单
- cron 服务在跑吗?
- 日志里有 CMD 记录吗?
- 用
bash -x手动模拟同样的环境跑一遍(env -i /bin/bash script.sh)。 - PATH、时区、工作目录对不对?
- 脚本权限、
%转义、输出重定向有没有问题? - 有没有并发重叠?
小结
cron 的环境几乎为空:显式设 PATH 和时区、用绝对路径、% 转义、记得 >> log 2>&1、用 flock 防重叠。先看日志确认有没有触发,再查脚本内部。