Linux服务器如何设置定时任务?Crontab配置与执行失败排查

"脚本手动跑得好好的,放进 crontab 就失败"——这句话大概是 Linux 运维里出现频率最高的抱怨之一。定时任务是服务器自动化运维的骨架:日志切割、数据库备份、证书续期、数据同步、监控采集,几乎都靠 cron 驱动。但 cron 的执行环境和你登录后的 shell 环境差异巨大,导致大量"本地能跑、定时跑不了"的怪事。本文从 crontab 的语法讲起,重点拆解五个时间字段的准确含义、环境变量陷阱以及执行失败的标准化排查流程。

一、crontab 的基本操作与文件位置

cron 是守护进程(CentOS 系叫 crond,Debian/Ubuntu 系叫 cron),它每分钟醒来一次,检查是否有任务到期。用户的定时任务写在 crontab 文件里。

# 查看当前用户的定时任务
crontab -l

# 编辑当前用户的定时任务(会做语法检查)
crontab -e

# 指定用户(root 专用)
crontab -u www -l
crontab -u www -e

# 删除当前用户所有定时任务(危险,无二次确认)
crontab -r

# 从文件导入(推荐用法,便于版本管理)
crontab /root/cron/backup.cron

# 备份现有任务
crontab -l > /root/cron/crontab.backup.$(date +%F)

相关文件位置必须记牢:

路径作用说明
/var/spool/cron/用户名用户级任务实际存放位置crontab -e 编辑的就是它,不建议直接改文件
/etc/crontab系统级主配置文件比用户 crontab 多一个"用户"字段
/etc/cron.d/系统级任务片段目录适合软件包投放自己的任务,格式同 /etc/crontab
/etc/cron.hourly/ 等按周期执行的脚本目录由 run-parts 调用,脚本需有可执行权限
/var/log/cronCentOS/RHEL 的 cron 日志记录"有没有执行",不记录脚本内部错误
/var/log/syslogUbuntu/Debian 的 cron 日志用 grep CRON /var/log/syslog 过滤
# 确认 cron 服务在运行
systemctl status crond     # CentOS/RHEL/Rocky
systemctl status cron      # Ubuntu/Debian

# 开机自启
systemctl enable --now crond

二、五个时间字段:一个都不能记错

用户 crontab 每行格式为:分 时 日 月 周 命令,共五个时间字段加一个命令。/etc/crontab 与 /etc/cron.d 下的文件多一个字段:分 时 日 月 周 用户 命令。

字段位置含义取值范围备注
第 1 个分钟(minute)0-59最常用的粒度,最小精度就是分钟
第 2 个小时(hour)0-2324 小时制,20 表示晚上 8 点
第 3 个日(day of month)1-31与"周"字段是 OR 关系,见下文
第 4 个月(month)1-12 或 JAN-DEC英文缩写不区分大小写
第 5 个周(day of week)0-7 或 SUN-SAT0 和 7 都表示周日,1=周一

2.1 特殊字符

  • *:每(每个单位都匹配)。
  • ,:列举,0,15,30,45 表示四个整刻点。
  • -:范围,9-18 表示 9 到 18。
  • /:步长,*/10 表示每 10 单位;*/5 * * * * 是每 5 分钟。
  • @reboot:系统启动时执行一次(很实用,但重启后服务顺序未必就绪)。
  • @daily / @midnight:等价于 0 0 * * *;@hourly 等价于 0 * * * *;@weekly 等价于 0 0 * * 0;@monthly 等价于 0 0 1 * *;@yearly 等价于 0 0 1 1 *。

2.2 常用写法示例

# 每天凌晨 2:00 执行数据库备份
0 2 * * * /opt/scripts/mysql_backup.sh >> /var/log/mysql_backup.log 2>&1

# 每 5 分钟执行一次监控采集
*/5 * * * * /opt/scripts/collect.sh

# 工作日(周一到周五)上午 9:30
30 9 * * 1-5 /opt/scripts/report.sh

# 每月 1 号凌晨 3:15
15 3 1 * * /opt/scripts/monthly.sh

# 每天 0 点和 12 点各一次
0 0,12 * * * /opt/scripts/sync.sh

# 开机后 1 分钟启动自研服务(需先测试就绪依赖)
@reboot sleep 60 && /opt/app/start.sh

# /etc/crontab 格式(多一个用户字段)
0 1 * * * root /usr/local/bin/clean_cache.sh

必须记住的坑:日字段和周字段是"或"关系,不是"且"。写成 0 0 1 * 1 意图是"每月 1 号且是周一",实际 cron 会在"每月 1 号"和"每周一"都执行。要实现"且",只能在脚本里用 date 判断:

0 0 1 * * /bin/bash -c '[ "$(date +\%u)" = "1" ] && /opt/scripts/monthly_monday.sh'

另外注意 % 在 crontab 里有特殊含义(表示换行,其后的内容作为标准输入传给命令),所以脚本里用到 date +%F 时必须转义为 \%,否则命令会被截断,这是极常见的失败原因。

三、为什么脚本手动能跑,cron 里就失败

这是本文的核心。原因有五类,按出现频率排序:

3.1 环境变量不同(第一大坑)

cron 执行时的 PATH 极其精简,通常是 /usr/bin:/bin,你装在 /usr/local/bin、/opt、或者用 nvm/pyenv 装的工具统统找不到。典型的报错是 "python3: command not found"、"node: command not found"。

# 查看 cron 实际的环境(写个任务把 env 打出来)
* * * * * env > /tmp/cron_env.txt 2>&1
# 等一分钟后查看
cat /tmp/cron_env.txt

解决办法三选一,推荐第一种:

  1. 命令一律写绝对路径:/usr/bin/python3 /opt/scripts/task.py。用 which python3 查真实路径。
  2. 在 crontab 文件顶部声明 PATH:PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,注意这是变量赋值不是任务行。
  3. 在脚本内 source 环境:source /etc/profile 或 export PATH=$PATH:/usr/local/bin。

同时可以在 crontab 里设置 SHELL=/bin/bash 和 MAILTO="ops@example.com",前者保证支持 bash 语法,后者让 cron 把输出邮寄出来(需要本机有邮件服务,也可改成重定向到文件)。

3.2 输出无处可去

cron 的任何 stdout/stderr 默认会尝试发邮件,没有邮件服务时就被丢弃,你完全看不到报错。所以每条任务都必须显式重定向输出:

# 标准写法:错误输出合并进标准输出,追加到日志
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# 丢弃输出(仅用于确定无价值的任务)
*/5 * * * * /opt/scripts/ping.sh >/dev/null 2>&1

# 分开记录标准输出与错误
0 3 * * * /opt/scripts/sync.sh >>/var/log/sync.out 2>>/var/log/sync.err

# 脚本内更彻底的调试:打开执行跟踪
0 3 * * * /bin/bash -x /opt/scripts/sync.sh >>/var/log/sync.debug 2>&1

3.3 相对路径问题

cron 执行时的当前工作目录是该用户的家目录,不是脚本所在目录。脚本里写 ./config.yaml 或 cd data && ... 都会失败。解决:脚本内第一行就 cd "$(dirname "$0")",或者全部使用绝对路径。

3.4 权限与解释器

脚本没加可执行权限(chmod +x),或者 shebang 写错(Windows 编辑过会产生 \r\n,导致 /bin/bash^M: bad interpreter)。检查方法:

chmod +x /opt/scripts/backup.sh
head -1 /opt/scripts/backup.sh        # 应是 #!/bin/bash
file /opt/scripts/backup.sh           # 若显示 CRLF 需转换
sed -i 's/\r$//' /opt/scripts/backup.sh

3.5 任务重叠与锁

备份脚本跑 40 分钟,而任务每 10 分钟一次,就会出现多个实例并发,互相踩踏。用 flock 加文件锁是最简单的方案:

# 加排他锁,抢不到锁就立即退出(等待 0 秒)
*/10 * * * * /usr/bin/flock -n /tmp/backup.lock /opt/scripts/backup.sh >>/var/log/backup.log 2>&1

# 等待最多 30 秒
*/10 * * * * /usr/bin/flock -w 30 /tmp/backup.lock /opt/scripts/backup.sh

3.6 时区与夏令时导致的"任务不执行"

cron 使用系统本地时间,这一点在多时区环境和夏令时地区会带来两类隐蔽问题。第一类是时区理解偏差:服务器时区设为 UTC,而你把任务写成 0 2 * * * 并以为它是"北京时间凌晨 2 点",实际会在 UTC 2 点(北京时间上午 10 点)执行,表现为"任务每天提前 8 小时跑"。用 timedatectl 可以查看 Local time 与 Universal time 的差值,修改用 timedatectl set-timezone Asia/Shanghai。改完必须重启 cron 服务(systemctl restart crond),因为 cron 在启动时会缓存时区信息,只改 /etc/localtime 而不重启服务,会出现 date 显示正确但任务执行时间错误的怪现象。

# 查看当前时区与时间
timedatectl
date

# 修改时区(改完重启 cron)
timedatectl set-timezone Asia/Shanghai
systemctl restart crond

第二类是夏令时:启用 DST 的地区在春季调时那天会直接跳过 2:00-3:00 这一小时,写在这个区间的任务当天完全不会执行;秋季调时则会出现同一个小时被走两次,任务可能执行两遍。国内使用 CST(UTC+8)不使用夏令时,不受影响,但如果你的业务覆盖海外机房或有境外节点,就要把关键任务避开 1:00-3:00 这个区间,或者干脆让服务器统一使用 UTC 并在文档里注明换算关系。判断依据:timedatectl | grep -i dst 若显示 "DST active: yes" 就需要考虑这个风险。

四、执行失败了怎么排查

cron 日志只记录"任务被触发了",不记录脚本内部发生了什么。排查顺序如下:

  1. 确认服务活着:systemctl status crond,不是 active (running) 就先启动。
  2. 确认任务被触发:CentOS 看 tail -f /var/log/cron,Ubuntu 看 grep CRON /var/log/syslog;systemd 系统也可以 journalctl -u cron -f。日志里出现 CMD (/opt/scripts/backup.sh) 说明触发了。
  3. 确认任务行存在且语法对:crontab -l,重点检查时间字段数量、百分号是否转义、命令是否绝对路径。
  4. 看脚本自己的日志:如果没重定向,先把重定向加上再等一轮。
  5. 手动复现 cron 环境,这是最有效的一步:
# 用与 cron 相同的精简环境执行脚本,能复现绝大多数失败
env -i PATH=/usr/bin:/bin SHELL=/bin/bash /bin/bash /opt/scripts/backup.sh

# 更严格:连 HOME 都清掉
env -i /bin/bash -c 'cd /root && /opt/scripts/backup.sh'
  1. 检查 SELinux / 审计:CentOS 上 SELinux 可能阻止脚本写某些目录,用 getenforce 看状态,ausearch -m avc -ts recent 查拒绝记录。
  2. 检查磁盘与 inode:磁盘满时 cron 会执行失败但不报错,先 df -h 与 df -i。
  3. 检查时间与时区:date、timedatectl,cron 用系统本地时间,改了时区要重启 cron 服务。

排查要点速查表

现象最可能原因验证命令
command not foundPATH 精简,未用绝对路径which 命令;env -i 复现
任务完全没执行crond 未运行 / 时间字段写错systemctl status crond;tail /var/log/cron
命令被截断% 未转义crontab -l 检查 \%
bad interpreterWindows 换行符 CRLFfile 脚本名
找不到配置文件工作目录不是脚本目录脚本内 cd "$(dirname "$0")"
结果重复/互相覆盖任务重叠执行flock -n 加锁
手动能跑,cron 下权限不足SELinux 或 sudo 需要 ttygetenforce;sudo 加 -n 并配 NOPASSWD

五、写好定时任务的实践规范

# 一个规范的 crontab 示例(crontab -e 内容)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# 每 5 分钟采集,带锁,输出归档
*/5 * * * * /usr/bin/flock -n /tmp/collect.lock /opt/scripts/collect.sh >> /var/log/collect.log 2>&1

# 每天 2:10 备份数据库
10 2 * * * /usr/bin/flock -n /tmp/dbbackup.lock /opt/scripts/mysql_backup.sh >> /var/log/dbbackup.log 2>&1

# 每周日凌晨 3:30 清理 30 天前归档
30 3 * * 0 /usr/bin/find /data/backup -name "*.gz" -mtime +30 -delete >> /var/log/cleanup.log 2>&1
  • 加注释:说明任务用途与负责人,半年后你会感谢自己。
  • 路径全绝对:脚本、解释器、被调用的二进制都写绝对路径。
  • 输出必重定向:没有日志的定时任务等于没有监控。
  • 加锁防重叠:凡是耗时可能超过执行间隔的任务都加 flock。
  • 加时间窗校验:备份类任务在脚本开头判断是否在业务低峰,防止时间改错导致白天执行。
  • 版本管理:把 crontab 导出到文件纳入 Git,避免误执行 crontab -r 后无法恢复。
  • 配置监控:关键备份任务建议配合健康检查(如 healthchecks 回调或日志关键字监控),做到"没跑起来也能报警"。

小结

设置 crontab 本身只要一分钟,难的是让它长期稳定、失败可查。记住四句话:时间字段是"分 时 日 月 周"且日月与周是或关系;百分号必须转义;命令一律绝对路径;输出必须重定向且长任务加 flock。排查时先确认"服务活不活 → 任务触发没 → 脚本报什么错",再用 env -i 复现 cron 环境,90% 的问题都会在这一步现形。

避坑清单

  • 不要直接编辑 /var/spool/cron/ 下的文件,用 crontab -e,它会做语法检查并正确通知守护进程重载。
  • 谨慎使用 crontab -r,它无确认直接清空;先 crontab -l > backup 再操作。
  • cron 的最小精度是分钟,需要秒级调度请用 systemd timer 或在脚本内 sleep 循环。
  • @reboot 不代表服务已就绪,网络、挂载、数据库可能还没起来,务必在脚本里加重试或 sleep。
  • sudo 在 cron 里常因 requiretty 失败,需要在 /etc/sudoers 配置 NOPASSWD 并使用 sudo -n。
  • 改时区后重启 cron,否则已加载的调度仍按旧时区计算。
  • 用户被锁定或密码过期时任务可能不执行,用 passwd -S 用户名 检查,服务账号建议 usermod -L 之外保持 shell 与密码状态正常。