定时任务谁都会用,但出问题的时候很多人抓瞎——任务没跑、跑了报错、跑了但没效果。
这篇把cron彻底讲清楚,包括怎么写、怎么调试、怎么排错。
crontab基础
编辑定时任务
# 编辑当前用户的crontab
crontab -e
# 查看当前用户的crontab
crontab -l
# 删除当前用户的所有crontab(危险)
crontab -r
# 编辑指定用户的crontab(需要root)
crontab -u nginx -e
时间格式
分 时 日 月 周 命令
* * * * * command
五个时间字段:
- 分:0-59
- 时:0-23
- 日:1-31
- 月:1-12
- 周:0-7(0和7都是周日)
常用写法
# 每分钟
* * * * * /path/to/script.sh
# 每小时的第30分钟
30 * * * * /path/to/script.sh
# 每天凌晨2点
0 2 * * * /path/to/script.sh
# 每天上午9点和下午6点
0 9,18 * * * /path/to/script.sh
# 每隔5分钟
*/5 * * * * /path/to/script.sh
# 每隔2小时
0 */2 * * * /path/to/script.sh
# 工作日每天9点
0 9 * * 1-5 /path/to/script.sh
# 每月1号凌晨
0 0 1 * * /path/to/script.sh
# 每周日凌晨3点
0 3 * * 0 /path/to/script.sh
特殊写法
@reboot /path/to/script.sh # 重启后执行一次
@yearly /path/to/script.sh # 每年1月1日0点
@monthly /path/to/script.sh # 每月1日0点
@weekly /path/to/script.sh # 每周日0点
@daily /path/to/script.sh # 每天0点
@hourly /path/to/script.sh # 每小时0分
容易踩的坑
坑1:环境变量
这是最常见的问题。
cron执行任务时的环境变量和你在终端里不一样。PATH可能只有/usr/bin:/bin,很多命令找不到。
解决方法1:用绝对路径
# 错误
0 2 * * * python /home/user/script.py
# 正确
0 2 * * * /usr/bin/python3 /home/user/script.py
解决方法2:在crontab开头定义环境变量
PATH=/usr/local/bin:/usr/bin:/bin
SHELL=/bin/bash
0 2 * * * python3 /home/user/script.py
解决方法3:在脚本开头source环境
#!/bin/bash
source /home/user/.bashrc
# 后面的代码...
坑2:工作目录
cron执行时的工作目录是用户home目录,不是脚本所在目录。
# 脚本里用相对路径会出问题
cd /home/user/project
python script.py # 找不到
# 正确做法:在脚本里cd
#!/bin/bash
cd /home/user/project || exit
python script.py
坑3:输出没处理
cron默认把输出发邮件。如果没配邮件,输出就丢了,出错也不知道。
# 把输出重定向到日志
0 2 * * * /path/to/script.sh >> /var/log/myjob.log 2>&1
# 如果不关心输出,丢到黑洞
0 2 * * * /path/to/script.sh > /dev/null 2>&1
2>&1是把标准错误也重定向到标准输出,别漏了。
坑4:权限问题
# 脚本没有执行权限
chmod +x /path/to/script.sh
# 或者用解释器调用
0 2 * * * /bin/bash /path/to/script.sh
坑5:特殊字符
crontab里%有特殊含义(换行),要转义:
# 错误
0 2 * * * echo "$(date +%Y-%m-%d)" >> /var/log/test.log
# 正确
0 2 * * * echo "$(date +\%Y-\%m-\%d)" >> /var/log/test.log
# 或者放到脚本里,脚本里不用转义
调试方法
手动执行测试
先在命令行里把命令跑一遍,确认没问题。
/bin/bash /path/to/script.sh
模拟cron环境
cron的环境很干净,可以模拟:
env -i /bin/bash --noprofile --norc -c '/path/to/script.sh'
如果这样跑不通,说明脚本依赖了某些环境变量。
查看cron日志
# Debian/Ubuntu
grep CRON /var/log/syslog
# CentOS/RHEL
grep CRON /var/log/cron
# 实时看
tail -f /var/log/syslog | grep CRON
能看到任务有没有被触发:
Dec 27 02:00:01 server CRON[12345]: (user) CMD (/path/to/script.sh)
给任务加日志
0 2 * * * /path/to/script.sh >> /var/log/myjob.log 2>&1
脚本里也加一些输出:
#!/bin/bash
echo "===== $(date) ====="
echo "开始执行..."
# 业务逻辑
echo "执行完成"
检查cron服务
# 看服务状态
systemctl status cron # Debian/Ubuntu
systemctl status crond # CentOS/RHEL
# 重启服务
systemctl restart cron
系统级crontab
除了用户的crontab,还有系统级的。
# 系统crontab文件
/etc/crontab
# 系统cron目录
/etc/cron.d/ # 自定义任务
/etc/cron.hourly/ # 每小时执行
/etc/cron.daily/ # 每天执行
/etc/cron.weekly/ # 每周执行
/etc/cron.monthly/ # 每月执行
/etc/crontab格式多一个用户字段:
# 分 时 日 月 周 用户 命令
0 2 * * * root /path/to/script.sh
往/etc/cron.daily/里放脚本,每天会自动执行。脚本不需要crontab格式,就是普通shell脚本,但要有执行权限。
进阶用法
任务不要重叠执行
如果任务跑的时间长,可能上一次还没跑完,下一次又开始了。
用flock加锁:
* * * * * flock -n /tmp/myjob.lock /path/to/script.sh
-n表示非阻塞,拿不到锁就直接退出。
或者在脚本里自己实现:
#!/bin/bash
LOCKFILE=/tmp/myjob.lock
if [ -f "$LOCKFILE" ]; then
echo "任务正在运行,退出"
exit 0
fi
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"
# 业务逻辑
随机延迟
避免所有机器同时跑任务,压力集中:
0 2 * * * sleep $((RANDOM \% 300)) && /path/to/script.sh
随机睡0-300秒再执行。
超时控制
防止任务跑太久:
0 2 * * * timeout 3600 /path/to/script.sh
超过1小时就kill掉。
通知执行结果
0 2 * * * /path/to/script.sh || echo "任务失败" | mail -s "cron告警" admin@example.com
或者用钉钉/飞书webhook:
#!/bin/bash
# script.sh
# 业务逻辑
result=$?
if [ $result -ne 0 ]; then
curl -s -X POST "https://oapi.dingtalk.com/robot/send?access_token=xxx" \
-H "Content-Type: application/json" \
-d '{"msgtype":"text","text":{"content":"定时任务执行失败"}}'
fi
常用场景
日志轮转
0 0 * * * find /var/log/myapp -name "*.log" -mtime +7 -delete
删除7天前的日志。
数据库备份
0 3 * * * mysqldump -u root -pxxx mydb | gzip > /backup/mydb_$(date +\%Y\%m\%d).sql.gz
同步文件
0 * * * * rsync -avz /data/ user@backup:/backup/data/
监控检查
*/5 * * * * /usr/local/bin/check_service.sh
清理临时文件
0 4 * * * find /tmp -type f -atime +3 -delete
cron本身不复杂,坑主要在环境变量和错误处理上。
记住几个原则:
- 用绝对路径
- 重定向输出到日志
- 加锁防止重叠
- 失败要有通知
这样基本就不会出问题了。