Linux定时任务cron完全指南:从写法到排错

257 阅读3分钟

定时任务谁都会用,但出问题的时候很多人抓瞎——任务没跑、跑了报错、跑了但没效果。

这篇把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本身不复杂,坑主要在环境变量和错误处理上。

记住几个原则:

  1. 用绝对路径
  2. 重定向输出到日志
  3. 加锁防止重叠
  4. 失败要有通知

这样基本就不会出问题了。