"脚本手动跑得好好的,放进 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/cron | CentOS/RHEL 的 cron 日志 | 记录"有没有执行",不记录脚本内部错误 |
| /var/log/syslog | Ubuntu/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-23 | 24 小时制,20 表示晚上 8 点 |
| 第 3 个 | 日(day of month) | 1-31 | 与"周"字段是 OR 关系,见下文 |
| 第 4 个 | 月(month) | 1-12 或 JAN-DEC | 英文缩写不区分大小写 |
| 第 5 个 | 周(day of week) | 0-7 或 SUN-SAT | 0 和 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
解决办法三选一,推荐第一种:
- 命令一律写绝对路径:
/usr/bin/python3 /opt/scripts/task.py。用which python3查真实路径。 - 在 crontab 文件顶部声明 PATH:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,注意这是变量赋值不是任务行。 - 在脚本内 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 日志只记录"任务被触发了",不记录脚本内部发生了什么。排查顺序如下:
- 确认服务活着:
systemctl status crond,不是 active (running) 就先启动。 - 确认任务被触发:CentOS 看
tail -f /var/log/cron,Ubuntu 看grep CRON /var/log/syslog;systemd 系统也可以journalctl -u cron -f。日志里出现CMD (/opt/scripts/backup.sh)说明触发了。 - 确认任务行存在且语法对:
crontab -l,重点检查时间字段数量、百分号是否转义、命令是否绝对路径。 - 看脚本自己的日志:如果没重定向,先把重定向加上再等一轮。
- 手动复现 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'
- 检查 SELinux / 审计:CentOS 上 SELinux 可能阻止脚本写某些目录,用
getenforce看状态,ausearch -m avc -ts recent查拒绝记录。 - 检查磁盘与 inode:磁盘满时 cron 会执行失败但不报错,先
df -h与df -i。 - 检查时间与时区:
date、timedatectl,cron 用系统本地时间,改了时区要重启 cron 服务。
排查要点速查表
| 现象 | 最可能原因 | 验证命令 |
|---|---|---|
| command not found | PATH 精简,未用绝对路径 | which 命令;env -i 复现 |
| 任务完全没执行 | crond 未运行 / 时间字段写错 | systemctl status crond;tail /var/log/cron |
| 命令被截断 | % 未转义 | crontab -l 检查 \% |
| bad interpreter | Windows 换行符 CRLF | file 脚本名 |
| 找不到配置文件 | 工作目录不是脚本目录 | 脚本内 cd "$(dirname "$0")" |
| 结果重复/互相覆盖 | 任务重叠执行 | flock -n 加锁 |
| 手动能跑,cron 下权限不足 | SELinux 或 sudo 需要 tty | getenforce;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 与密码状态正常。
企业QQ咨询




