一、概述
1.1 背景介绍
Crontab 是 Linux 系统下最经典的定时任务工具,但它也是线上事故的“高发区”。环境变量丢失、任务重复执行、输出未重定向导致邮件队列爆满——这些坑,几乎每个运维工程师都踩过。我们团队就曾处理过一起线上事故:一个备份脚本没有加文件锁,凌晨2点触发后因为执行时间超过了1小时,第二次调度又启动了一个新实例,两个进程同时写入同一个备份文件,最终导致备份数据损坏。自那以后,团队立下规矩:所有 Cron 任务必须加锁、必须重定向输出、必须记录执行日志。
Cron 本身的机制并不复杂:crond 守护进程每分钟醒来一次,检查所有 crontab 配置,将到时间的任务拉起执行。但简单不代表不会出问题,恰恰是因为太简单,很多细节容易被忽略。比如,Cron 的执行环境和你通过 SSH 登录后的 Shell 环境完全不同,其 PATH 通常只有 /usr/bin:/bin,很多自定义命令会找不到;再比如,Cron 任务的 stdout 和 stderr 默认会通过邮件发送给用户,如果不做处理,/var/spool/mail 目录很快就会被撑爆。
这篇文章将系统性地梳理 Crontab,内容从基础语法一直延伸到生产级别的实践经验,其中也包括 systemd timer 这个现代替代方案。所有配置和脚本均在 CentOS 7/8 和 Ubuntu 20.04/22.04 上验证通过。
1.2 技术特点
- 基于
crond 守护进程的周期调度:crond 是一个常驻后台的守护进程,每分钟检查一次所有 crontab 配置。其调度精度是分钟级,不支持秒级调度(需要秒级调度需采用其他方案)。crond 本身资源占用极低,常驻内存通常不到 2MB,CPU 使用几乎为零,稳定性极好。
- 支持用户级和系统级两种配置方式:
- 用户级 crontab:通过
crontab -e 编辑,每个用户独立,存储在 /var/spool/cron/用户名(CentOS/RHEL)或 /var/spool/cron/crontabs/用户名(Ubuntu/Debian)。
- 系统级 crontab:位于
/etc/crontab 和 /etc/cron.d/ 目录下,语法上比用户级多了一个“用户名字段”,更适合部署系统维护任务。
- 时间表达式灵活:5个时间字段(分、时、日、月、周)配合特殊字符(
*,/, ,, -)可以表达绝大多数调度需求,例如每5分钟、工作日的工作时间、每月1号和15号执行等。但对于“每月最后一个工作日”这类复杂逻辑,Crontab 表达起来比较别扭,此时 systemd timer 更为合适。
1.3 适用场景
- 定期维护任务:日志清理、临时文件清理、数据库备份、证书续期检查等。这类任务执行频率低(每天或每周一次),对时间精度要求不高,Crontab 完全够用。
- 周期性数据处理:定时拉取数据、生成报表、同步文件、推送通知等。这类任务通常有固定的执行周期,需要注意任务执行时间不能超过调度间隔,否则会出现任务堆积。
- 监控和巡检:定时检查服务状态、磁盘空间、证书过期时间、进程存活等。这类任务执行频率高(每1-5分钟),要求脚本轻量,执行时间控制在几秒内。对于更精细的监控,建议使用 Prometheus、Zabbix 等专业监控系统,Crontab 可作为补充。
1.4 环境要求
| 组件 |
版本要求 |
说明 |
| 操作系统 |
CentOS 6+/RHEL 6+/Ubuntu 16.04+ |
基本所有主流 Linux 发行版都自带 Cron |
| cronie |
1.4.11+ (CentOS 7 自带) |
CentOS/RHEL 使用 cronie 包,Ubuntu 使用 cron 包 |
| flock |
util-linux 2.23+ (系统自带) |
文件锁工具,用于防止任务重复执行 |
| systemd |
219+ (CentOS 7+) |
如需使用 systemd timer 替代方案 |
| 邮件服务 |
postfix/sendmail (可选) |
Cron 默认通过邮件发送任务输出,不安装则需重定向输出 |
二、详细步骤
2.1 准备工作
2.1.1 系统检查
# 检查 cron 服务是否安装和运行
# CentOS/RHEL
rpm -qa | grep cronie
systemctl status crond
# Ubuntu/Debian
dpkg -l | grep cron
systemctl status cron
# 检查当前用户的 crontab
crontab -l
# 检查系统级 crontab
cat /etc/crontab
ls -la /etc/cron.d/
ls -la /etc/cron.daily/
ls -la /etc/cron.hourly/
# 检查 cron 日志(确认 cron 在正常工作)
# CentOS/RHEL
tail -20 /var/log/cron
# Ubuntu/Debian
grep CRON /var/log/syslog | tail -20
# 检查是否有用户被禁止使用 cron
cat /etc/cron.allow 2>/dev/null
cat /etc/cron.deny 2>/dev/null
2.1.2 安装依赖
# CentOS/RHEL - 安装 cronie(通常已预装)
sudo yum install -y cronie
# Ubuntu/Debian - 安装 cron(通常已预装)
sudo apt install -y cron
# 确认 flock 可用(防重复执行用)
which flock
flock --version
# 启动 cron 服务
# CentOS/RHEL
sudo systemctl start crond
sudo systemctl enable crond
# Ubuntu/Debian
sudo systemctl start cron
sudo systemctl enable cron
2.2 核心配置
2.2.1 crontab 时间表达式详解
Crontab 的时间表达式由 5 个字段组成,用空格分隔:
┌───────────── 分钟 (0-59)
│ ┌───────────── 小时 (0-23)
│ │ ┌───────────── 日 (1-31)
│ │ │ ┌───────────── 月 (1-12)
│ │ │ │ ┌───────────── 星期 (0-7,0和7都是周日)
│ │ │ │ │
* * * * * command
| 特殊字符: |
字符 |
含义 |
示例 |
* |
任意值 |
* * * * * 每分钟 |
, |
列举多个值 |
0,30 * * * * 每小时的0分和30分 |
- |
范围 |
0 9-17 * * * 每天9点到17点的整点 |
/ |
步长 |
*/5 * * * * 每5分钟 |
常用时间表达式速查:
# 每分钟执行
* * * * *
# 每5分钟执行
*/5 * * * *
# 每小时整点执行
0 * * * *
# 每天凌晨2点执行
0 2 * * *
# 每天凌晨2点30分执行
30 2 * * *
# 每周一凌晨3点执行
0 3 * * 1
# 每周一到周五的9点到18点,每小时执行
0 9-18 * * 1-5
# 每月1号凌晨4点执行
0 4 1 * *
# 每月1号和15号凌晨4点执行
0 4 1,15 * *
# 每季度第一天凌晨5点执行(1月、4月、7月、10月的1号)
0 5 1 1,4,7,10 *
# 每年1月1日凌晨0点执行
0 0 1 1 *
# 每天8点到20点,每2小时执行
0 8-20/2 * * *
# 每周日凌晨1点执行
0 1 * * 0
一个重要提醒:“日”和“星期”字段是“或”(OR) 的关系,不是“且”(AND)。例如 0 2 15 * 5 表示“每月15号 或者 每周五的凌晨2点都会执行”,而不是“每月15号且是周五”。这个逻辑与直觉不符,编写复杂表达式时需要特别注意。
2.2.2 用户级 crontab 操作
# 编辑当前用户的 crontab
crontab -e
# 查看当前用户的 crontab
crontab -l
# 删除当前用户的所有 crontab(危险操作,会清空所有任务!)
# 生产环境建议先 `crontab -l > /tmp/crontab.bak` 备份
crontab -r
# 从文件导入 crontab
crontab /path/to/crontab-file
# 编辑指定用户的 crontab(需要 root 权限)
sudo crontab -u nginx -e
sudo crontab -u nginx -l
用户级 crontab 文件不要直接编辑,应使用 crontab -e 命令,它会进行语法检查。
2.2.3 系统级 crontab
系统级 crontab 主要有三个位置:
-
/etc/crontab - 系统主 crontab 文件:
# /etc/crontab - 系统级 crontab
# 注意:比用户级 crontab 多了一个“用户名”字段
SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin
MAILTO=root
# 分 时 日 月 周 用户名 命令
# 每小时执行 run-parts(执行目录下所有脚本)
01 * * * * root run-parts /etc/cron.hourly
02 4 * * * root run-parts /etc/cron.daily
22 4 * * 0 root run-parts /etc/cron.weekly
42 4 1 * * root run-parts /etc/cron.monthly
-
/etc/cron.d/ - 系统级 crontab 片段目录:
# /etc/cron.d/backup-task
# 格式和 /etc/crontab 一样,多了用户名字段
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 2 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
-
/etc/cron.{hourly,daily,weekly,monthly}/ - 周期执行目录:
# 这些目录下放可执行脚本,由 run-parts 命令按周期执行
# 脚本不需要 crontab 时间表达式,放进去就行
# 注意:脚本文件名不能包含“.”(点号),否则 run-parts 会跳过
ls -la /etc/cron.daily/
# 典型内容:logrotate, man-db, mlocate 等系统维护脚本
2.2.4 环境变量问题(重点)
Cron 的执行环境与你 SSH 登录后的 Shell 环境完全不同,这是 Cron 任务出问题的头号原因。
# cron 默认的环境变量非常少
# PATH=/usr/bin:/bin(CentOS)
# PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin(Ubuntu)
# HOME=用户家目录
# SHELL=/bin/sh
# LOGNAME=用户名
# 验证 cron 的实际环境变量(加一条临时任务)
* * * * * env > /tmp/cron-env.txt
# 等1分钟后查看
cat /tmp/cron-env.txt
# 看完记得删掉这条临时任务
解决方案一:在脚本开头加载环境(推荐):
#!/bin/bash
# 在脚本开头 source 环境配置
source /etc/profile
source ~/.bashrc
# 或者直接设置需要的 PATH
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 后面的命令就可以正常找到了
java -version
node --version
python3 --version
解决方案二:在 crontab 中定义环境变量:
# 在 crontab 文件开头定义
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/java/bin
MAILTO=""
HOME=/home/deploy
# 任务定义
0 2 * * * /usr/local/bin/backup.sh
解决方案三:在命令中使用绝对路径:
# 不要写
0 2 * * * backup.sh
# 要写绝对路径
0 2 * * * /usr/local/bin/backup.sh
# 或者用 env 指定
0 2 * * * /usr/bin/env bash /usr/local/bin/backup.sh
我们团队的做法是三种方案组合:crontab 里定义 PATH 和 SHELL,脚本里也 source /etc/profile 做兜底,命令一律用绝对路径。三重保险,基本杜绝了环境变量问题。
2.2.5 输出重定向
Cron 任务的 stdout 和 stderr 默认会通过邮件发送给 MAILTO 指定的用户。如果没装邮件服务,输出会堆积在 /var/spool/mail/ 下。一个每分钟执行的脚本,如果每次输出100行,一个月下来邮件文件可能达到几百MB。
# 错误写法 - 没有重定向,输出会变成邮件
* * * * * /usr/local/bin/check.sh
# 正确写法 - 标准输出和错误输出都重定向到日志文件
* * * * * /usr/local/bin/check.sh >> /var/log/check.log 2>&1
# 如果确实不需要任何输出
* * * * * /usr/local/bin/check.sh > /dev/null 2>&1
# 标准输出丢弃,只保留错误输出
* * * * * /usr/local/bin/check.sh > /dev/null 2>> /var/log/check-error.log
# 在 crontab 开头设置 MAILTO 为空,禁用邮件通知
MAILTO=""
生产环境建议:所有 Cron 任务都必须重定向输出到日志文件,不要简单丢弃到 /dev/null。出了问题没有日志可查,排障时会非常被动。同时,日志文件要配置 logrotate 进行轮转,避免日志本身撑满磁盘。
2.2.6 任务防重复执行(flock 文件锁)
当任务执行时间超过调度间隔时,会出现多个实例同时运行的情况。使用 flock 文件锁可以有效防止这个问题。
# flock 基本用法
# -x 排他锁(默认)
# -n 非阻塞,获取不到锁立即退出(不等待)
# -w 超时时间(秒)
# 方式一:直接在 crontab 中使用 flock
*/5 * * * * flock -xn /tmp/backup.lock -c '/usr/local/bin/backup.sh >> /var/log/backup.log 2>&1'
# 方式二:在脚本内部使用 flock
#!/bin/bash
LOCK_FILE="/tmp/$(basename $0).lock"
exec 200>"$LOCK_FILE"
flock -xn 200 || { echo "另一个实例正在运行,退出"; exit 1; }
# 正常业务逻辑
echo "开始执行..."
# ...
# 方式三:flock 包装整个脚本(推荐,最简洁)
#!/bin/bash
[ "${FLOCKER}" != "$0" ] && exec env FLOCKER="$0" flock -xn "$0" "$0" "$@" || :
# 到这里说明已经获取到锁了
echo "开始执行..."
关于 flock 锁文件的路径:
- 建议使用
/tmp 下的文件,系统重启后锁文件消失,不影响后续执行。
- 锁文件名要和任务对应,不同任务使用不同的锁文件。
- 避免使用
/var/lock/,部分系统会定期清理此目录。
2.2.7 systemd timer 替代方案
对于 CentOS 7+ 和 Ubuntu 16.04+ 的系统,可以使用 systemd timer 作为 Crontab 的替代方案。它的优势包括:支持秒级精度、自动记录日志到 journald、支持任务依赖关系、可以设置 CPU/内存等资源限制。
创建一个 systemd timer:
需要两个文件:一个 .service 文件定义要执行的任务,一个 .timer 文件定义调度时间。
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup task
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=root
# 资源限制(crontab 做不到的)
MemoryMax=512M
CPUQuota=50%
# 超时控制
TimeoutStartSec=3600
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 2:00 AM
[Timer]
# 每天凌晨2点执行
OnCalendar=*-*-* 02:00:00
# 如果错过了执行时间(比如机器关机了),启动后立即补执行一次
Persistent=true
# 随机延迟0-5分钟,避免多台机器同时执行
RandomizedDelaySec=300
# 精度,默认1分钟,可设置为1秒
AccuracySec=1s
[Install]
WantedBy=timers.target
# 启用 timer
sudo systemctl daemon-reload
sudo systemctl enable backup.timer
sudo systemctl start backup.timer
# 查看 timer 状态
systemctl status backup.timer
systemctl list-timers --all
# 手动触发一次(测试用)
sudo systemctl start backup.service
# 查看执行日志
journalctl -u backup.service --since "1 hour ago"
OnCalendar 时间表达式:
# systemd timer 的时间表达式比 crontab 更直观
OnCalendar=*-*-* 02:00:00 # 每天2点
OnCalendar=Mon *-*-* 03:00:00 # 每周一3点
OnCalendar=*-*-01 04:00:00 # 每月1号4点
OnCalendar=*-01,04,07,10-01 05:00 # 每季度第一天5点
OnCalendar=*-*-* *:00/5:00 # 每5分钟(注意格式)
OnCalendar=hourly # 每小时
OnCalendar=daily # 每天
OnCalendar=weekly # 每周
OnCalendar=monthly # 每月
# 验证时间表达式
systemd-analyze calendar "*-*-* 02:00:00"
systemd-analyze calendar "Mon *-*-* 03:00:00" --iterations=5
| Crontab vs systemd timer 对比: |
特性 |
crontab |
systemd timer |
| 最小精度 |
分钟 |
秒(微秒) |
| 日志 |
需要自己重定向 |
自动记录到 journald |
| 防重复 |
需要 flock |
默认不会重复(oneshot 类型) |
| 资源限制 |
不支持 |
支持 CPU/内存/IO 限制 |
| 错过补执行 |
不支持 |
Persistent=true |
| 随机延迟 |
不支持 |
RandomizedDelaySec |
| 依赖关系 |
不支持 |
After/Requires |
| 配置复杂度 |
一行搞定 |
需要两个文件 |
| 学习成本 |
低 |
中等 |
| 通用性 |
所有 Linux |
需要 systemd |
选择标准:简单的定时任务用 Crontab(一行配置搞定),需要资源限制、精确控制、自动日志的复杂任务用 systemd timer。两者并不冲突,可以在系统中并存。
2.2.8 anacron 补充执行
anacron 用于处理机器关机期间错过的定时任务。Crontab 的任务如果到了执行时间机器没开机,这次执行就会被跳过。anacron 会在机器启动后检查哪些任务错过了,并补执行一次。
# anacron 配置文件
cat /etc/anacrontab
# 格式:周期(天) 延迟(分钟) 任务标识 命令
# period delay job-identifier command
1 5 cron.daily nice run-parts /etc/cron.daily
7 25 cron.weekly nice run-parts /etc/cron.weekly
@monthly 45 cron.monthly nice run-parts /etc/cron.monthly
anacron 的时间戳文件位于 /var/spool/anacron/,记录了每个任务最后执行的日期。生产环境中,服务器一般不会关机,anacron 主要用在开发机和笔记本上。但如果你的服务器有计划内的重启维护窗口,anacron 可以确保日常维护任务不会因为重启而遗漏。
2.3 启动和验证
2.3.1 启动服务
# CentOS/RHEL - 启动 crond
sudo systemctl start crond
sudo systemctl enable crond
sudo systemctl status crond
# Ubuntu/Debian - 启动 cron
sudo systemctl start cron
sudo systemctl enable cron
sudo systemctl status cron
# 查看 cron 服务日志
# CentOS/RHEL
tail -20 /var/log/cron
# Ubuntu/Debian
journalctl -u cron --since "10 minutes ago"
2.3.2 功能验证
# 1. 添加一条测试任务(每分钟执行)
crontab -e
# 添加这行:
# * * * * * echo "cron test $(date)" >> /tmp/cron-test.log
# 2. 等待1-2分钟后检查
cat /tmp/cron-test.log
# 应该看到类似:
# cron test Sun Jan 15 10:01:01 CST 2024
# cron test Sun Jan 15 10:02:01 CST 2024
# 3. 确认 cron 日志中有执行记录
# CentOS
grep "cron test" /var/log/cron | tail -5
# Ubuntu
grep CRON /var/log/syslog | tail -5
# 4. 测试 flock 防重复
echo '#!/bin/bash
echo "start $(date)" >> /tmp/flock-test.log
sleep 120
echo "end $(date)" >> /tmp/flock-test.log' > /tmp/test-flock.sh
chmod +x /tmp/test-flock.sh
# 在 crontab 中添加(每分钟执行,但脚本要跑2分钟)
# * * * * * flock -xn /tmp/test-flock.lock -c '/tmp/test-flock.sh'
# 等3分钟后检查,应该只有一个 start(因为第二次被锁挡住了)
cat /tmp/flock-test.log
# 5. 验证完毕,清理测试任务
crontab -e # 删除测试行
rm -f /tmp/cron-test.log /tmp/flock-test.log /tmp/test-flock.sh /tmp/cron-env.txt
三、示例代码和配置
3.1 完整配置示例
3.1.1 生产环境 crontab 完整配置
这是一份经过线上大量服务器验证的标准模板,覆盖了备份、清理、监控、同步等常见场景。每条任务都配备了文件锁、日志记录和输出重定向。
# /var/spool/cron/root - 生产环境标准 crontab
# 最后更新:2024-01-15
# 维护人:ops-team
# === 环境变量 ===
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
HOME=/root
# === 系统维护类 ===
# 每天凌晨2:10 - 数据库备份(MySQL全量备份)
10 2 * * * flock -xn /tmp/db-backup.lock -c '/usr/local/bin/db-backup.sh >> /var/log/cron-jobs/db-backup.log 2>&1'
# 每天凌晨3:00 - 文件备份(配置文件和应用数据)
0 3 * * * flock -xn /tmp/file-backup.lock -c '/usr/local/bin/file-backup.sh >> /var/log/cron-jobs/file-backup.log 2>&1'
# 每天凌晨4:30 - 清理30天前的临时文件和日志
30 4 * * * /usr/bin/find /tmp -type f -mtime +7 -delete 2>/dev/null; /usr/bin/find /var/log/cron-jobs -name "*.log" -mtime +30 -delete 2>/dev/null
# 每周日凌晨5:00 - 系统更新检查(只检查不安装)
0 5 * * 0 /usr/bin/yum check-update > /var/log/cron-jobs/yum-check.log 2>&1 || true
# === 监控巡检类 ===
# 每5分钟 - 磁盘空间检查
*/5 * * * * /usr/local/bin/check-disk.sh >> /var/log/cron-jobs/disk-check.log 2>&1
# 每分钟 - 关键服务存活检查
* * * * * /usr/local/bin/check-services.sh >> /var/log/cron-jobs/service-check.log 2>&1
# 每10分钟 - SSL证书过期检查
*/10 * * * * /usr/local/bin/check-ssl.sh >> /var/log/cron-jobs/ssl-check.log 2>&1
# === 数据同步类 ===
# 每小时整点 - 同步配置到备机
0 * * * * flock -xn /tmp/config-sync.lock -c '/usr/local/bin/config-sync.sh >> /var/log/cron-jobs/config-sync.log 2>&1'
# 每天凌晨1:00 - 日志归档到远程存储
0 1 * * * flock -xn /tmp/log-archive.lock -c '/usr/local/bin/log-archive.sh >> /var/log/cron-jobs/log-archive.log 2>&1'
# === 业务相关类 ===
# 每天8:00 - 生成日报并发送
0 8 * * 1-5 /usr/local/bin/daily-report.sh >> /var/log/cron-jobs/daily-report.log 2>&1
# 每月1号凌晨6:00 - 生成月度统计报表
0 6 1 * * /usr/local/bin/monthly-report.sh >> /var/log/cron-jobs/monthly-report.log 2>&1
# === crontab 自身备份 ===
# 每天凌晨0:30 - 备份所有用户的 crontab
30 0 * * * /usr/bin/crontab -l > /var/backup/crontab/root-$(date +\%Y\%m\%d).txt 2>&1
重要提醒:在 crontab 中,% 是特殊字符(表示换行)。在命令中使用 % 必须用 \% 转义。例如 date +%Y%m%d 在 crontab 里必须写成 date +\%Y\%m\%d,否则会报错。
3.1.2 带锁机制和日志的定时任务包装脚本
这个包装脚本可以作为所有 Cron 任务的标准入口,统一处理锁、日志、超时和告警,是提升 运维 效率的好工具。
#!/bin/bash
# 文件名:/usr/local/bin/cron-wrapper.sh
# 功能:cron 任务包装器 - 统一处理锁、日志、超时、告警
# 用法:cron-wrapper.sh <任务名> <超时秒数> <实际命令>
# 示例:cron-wrapper.sh db-backup 3600 /usr/local/bin/db-backup.sh
set -euo pipefail
TASK_NAME="${1:?用法: $0 <任务名> <超时秒数> <命令>}"
TIMEOUT="${2:?缺少超时参数}"
shift 2
COMMAND="$@"
# 配置
LOG_DIR="/var/log/cron-jobs"
LOCK_DIR="/tmp/cron-locks"
ALERT_WEBHOOK="http://monitor.internal.com/api/alert"
# 初始化
mkdir -p "$LOG_DIR" "$LOCK_DIR"
LOG_FILE="${LOG_DIR}/${TASK_NAME}.log"
LOCK_FILE="${LOCK_DIR}/${TASK_NAME}.lock"
START_TIME=$(date +%s)
START_DATE=$(date '+%Y-%m-%d %H:%M:%S')
# 日志函数
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 告警函数(发送到监控系统)
send_alert() {
local level="$1"
local message="$2"
curl -s -X POST "$ALERT_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"level\":\"${level}\",\"task\":\"${TASK_NAME}\",\"host\":\"$(hostname)\",\"message\":\"${message}\"}" \
> /dev/null 2>&1 || true
}
# 获取文件锁
exec 200>"$LOCK_FILE"
if ! flock -xn 200; then
log "SKIP: 另一个实例正在运行,跳过本次执行"
exit 0
fi
log "START: 开始执行 [${COMMAND}]"
# 带超时执行命令
EXIT_CODE=0
timeout "$TIMEOUT" bash -c "$COMMAND" >> "$LOG_FILE" 2>&1 || EXIT_CODE=$?
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
# 记录执行结果
if [ $EXIT_CODE -eq 0 ]; then
log "SUCCESS: 执行完成,耗时 ${DURATION}秒"
elif [ $EXIT_CODE -eq 124 ]; then
log "TIMEOUT: 执行超时(超过${TIMEOUT}秒),已被终止"
send_alert "critical" "任务超时: ${TASK_NAME},超时阈值${TIMEOUT}秒"
else
log "FAILED: 执行失败,退出码 ${EXIT_CODE},耗时 ${DURATION}秒"
send_alert "warning" "任务失败: ${TASK_NAME},退出码${EXIT_CODE}"
fi
# 写入执行摘要(方便监控采集)
echo "${START_DATE},${TASK_NAME},${EXIT_CODE},${DURATION}" >> "${LOG_DIR}/execution-summary.csv"
exit $EXIT_CODE
在 crontab 中使用这个包装器:
# 数据库备份,超时1小时
10 2 * * * /usr/local/bin/cron-wrapper.sh db-backup 3600 /usr/local/bin/db-backup.sh
# 磁盘检查,超时30秒
*/5 * * * * /usr/local/bin/cron-wrapper.sh disk-check 30 /usr/local/bin/check-disk.sh
# 配置同步,超时10分钟
0 * * * * /usr/local/bin/cron-wrapper.sh config-sync 600 /usr/local/bin/config-sync.sh
3.2 实际应用案例
案例一:systemd timer 配置案例 - 数据库定时清理
场景描述:每天凌晨3点清理数据库中超过90天的历史数据,需要限制内存使用不超过256MB,执行超时2小时自动终止。Crontab 无法实现资源限制,因此采用 systemd timer。
Service 文件:
# /etc/systemd/system/db-cleanup.service
[Unit]
Description=Database old data cleanup
After=mysql.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/db-cleanup.sh
User=root
MemoryMax=256M
CPUQuota=30%
TimeoutStartSec=7200
StandardOutput=journal
StandardError=journal
SyslogIdentifier=db-cleanup
Timer 文件:
# /etc/systemd/system/db-cleanup.timer
[Unit]
Description=Run database cleanup daily at 3:00 AM
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=120
AccuracySec=1min
[Install]
WantedBy=timers.target
清理脚本:
#!/bin/bash
# /usr/local/bin/db-cleanup.sh
set -euo pipefail
MYSQL_CMD="mysql -u cleanup_user -p'Cleanup@2024' -h 127.0.0.1"
RETENTION_DAYS=90
DELETE_BATCH=5000
echo "[$(date)] 开始清理${RETENTION_DAYS}天前的历史数据..."
# 分批删除,避免长事务锁表
TOTAL_DELETED=0
while true; do
DELETED=$($MYSQL_CMD -N -e "
DELETE FROM app_db.operation_log
WHERE created_at < DATE_SUB(NOW(), INTERVAL ${RETENTION_DAYS} DAY)
LIMIT ${DELETE_BATCH};
SELECT ROW_COUNT();
")
TOTAL_DELETED=$((TOTAL_DELETED + DELETED))
echo "本批删除 ${DELETED} 条,累计 ${TOTAL_DELETED} 条"
if [ "$DELETED" -lt "$DELETE_BATCH" ]; then
break
fi
# 每批之间暂停1秒,降低数据库压力
sleep 1
done
echo "[$(date)] 清理完成,共删除 ${TOTAL_DELETED} 条记录"
启用和验证:
sudo systemctl daemon-reload
sudo systemctl enable db-cleanup.timer
sudo systemctl start db-cleanup.timer
# 查看 timer 状态
systemctl list-timers | grep db-cleanup
# 手动测试一次
sudo systemctl start db-cleanup.service
journalctl -u db-cleanup.service --since "5 minutes ago"
案例二:crontab 任务监控和告警脚本
场景描述:监控所有 Cron 任务的执行情况,发现任务失败或超时及时告警。该脚本配合 cron-wrapper.sh 生成的执行摘要 CSV 文件使用。
#!/bin/bash
# 文件名:/usr/local/bin/cron-monitor.sh
# 功能:监控 cron 任务执行状态,异常时发送告警
# 依赖:cron-wrapper.sh 生成的 execution-summary.csv
# crontab: */5 * * * * /usr/local/bin/cron-monitor.sh
set -euo pipefail
SUMMARY_FILE="/var/log/cron-jobs/execution-summary.csv"
ALERT_LOG="/var/log/cron-jobs/alert-history.log"
WEBHOOK_URL="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR-KEY"
# 检查最近5分钟的执行记录
FIVE_MIN_AGO=$(date -d '5 minutes ago' '+%Y-%m-%d %H:%M:%S')
if [ ! -f "$SUMMARY_FILE" ]; then
echo "摘要文件不存在:$SUMMARY_FILE"
exit 0
fi
# 解析最近5分钟的失败任务
FAILED_TASKS=""
while IFS=',' read -r exec_time task_name exit_code duration; do
# 跳过时间早于5分钟前的记录
if [[ "$exec_time" < "$FIVE_MIN_AGO" ]]; then
continue
fi
# 检查失败(退出码非0)
if [ "$exit_code" != "0" ]; then
if [ "$exit_code" == "124" ]; then
STATUS="TIMEOUT"
else
STATUS="FAILED(exit=$exit_code)"
fi
FAILED_TASKS="${FAILED_TASKS}\n- ${task_name}: ${STATUS}, 耗时${duration}秒, 时间${exec_time}"
fi
done < "$SUMMARY_FILE"
# 发送告警
if [ -n "$FAILED_TASKS" ]; then
HOSTNAME=$(hostname)
MESSAGE="[Cron告警] ${HOSTNAME}\n异常任务:${FAILED_TASKS}"
echo "[$(date)] 发送告警: $MESSAGE" >> "$ALERT_LOG"
# 企业微信机器人告警
curl -s -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{
\"msgtype\": \"text\",
\"text\": {
\"content\": \"$(echo -e \"$MESSAGE\")\"
}
}" > /dev/null 2>&1
fi
# 检查关键任务是否按时执行(漏执行检测)
check_task_executed() {
local task_name="$1"
local expected_hour="$2"
local today=$(date '+%Y-%m-%d')
if ! grep -q "${today}.*${task_name}.*,0," "$SUMMARY_FILE" 2>/dev/null; then
# 只在预期执行时间之后检查
local current_hour=$(date +%H)
if [ "$current_hour" -gt "$expected_hour" ]; then
echo "[$(date)] 警告: ${task_name} 今天未成功执行" >> "$ALERT_LOG"
fi
fi
}
# 检查关键任务(根据实际情况配置)
check_task_executed "db-backup" 3
check_task_executed "file-backup" 4
运行结果示例 (/var/log/cron-jobs/alert-history.log):
[2024-01-15 02:15:01] 发送告警: [Cron告警] web-server-01
异常任务:
- db-backup: TIMEOUT, 耗时3601秒, 时间2024-01-15 02:10:01
[2024-01-15 08:05:01] 警告: file-backup 今天未成功执行
四、最佳实践和注意事项
4.1 最佳实践
4.1.1 性能优化
-
大任务错峰执行:避免多个重量级任务安排在同一时间。例如,将数据库备份、日志归档、数据同步等任务错开至凌晨2:00、3:00、4:00分别执行,可以有效避免磁盘 I/O 争抢。
# 错误示范 - 所有重任务挤在一起
0 2 * * * /usr/local/bin/db-backup.sh
0 2 * * * /usr/local/bin/log-archive.sh
0 2 * * * /usr/local/bin/data-sync.sh
# 正确做法 - 错峰安排
0 2 * * * /usr/local/bin/db-backup.sh # 2:00 数据库备份
0 3 * * * /usr/local/bin/log-archive.sh # 3:00 日志归档
0 4 * * * /usr/local/bin/data-sync.sh # 4:00 数据同步
- 高频任务脚本要轻量:对于每分钟或每5分钟执行的监控脚本,执行时间必须控制在调度间隔的1/3以内,避免任务堆积。可以通过优化脚本逻辑(例如用
/proc 文件系统代替外部命令)来降低开销。
- 使用
ionice 和 nice 降低任务优先级:对于备份、归档等非紧急任务,可以降低其 CPU 和 I/O 优先级,减少对在线业务的影响。
# nice -n 19 最低CPU优先级,ionice -c 3 空闲IO调度
0 2 * * * nice -n 19 ionice -c 3 /usr/local/bin/backup.sh >> /var/log/cron-jobs/backup.log 2>&1
4.1.2 安全加固
4.1.3 高可用配置
- Crontab 版本管理和备份:Crontab 没有版本历史,误删难以恢复。建议定期导出备份,并纳入 Git 等版本控制系统管理。
- 关键任务双机部署:对于不能中断的定时任务(如监控告警),可在主备两台机器上都配置,通过分布式锁(Redis/etcd)或简单的主备检测机制(如检测哪个机器持有 VIP)来避免重复执行。
- Crontab 变更审计:通过 Shell 函数包装或审计工具,记录谁在什么时候修改了 crontab,便于问题追溯。
4.2 注意事项
4.2.1 配置注意事项
% 字符必须转义:在 crontab 中,% 是特殊字符,表示换行。命令中用到 % 必须用 \% 转义,最常见于 date 命令。
- 慎用
crontab -r:该命令会直接删除当前用户的所有 crontab 任务,没有确认提示。执行前务必先使用 crontab -l > /tmp/crontab-backup.txt 备份。
- 注意工作目录:Cron 任务的工作目录是用户的
HOME 目录,而非脚本所在目录。脚本中所有文件路径都应使用绝对路径。
- 配置生效时间:新添加的用户级 crontab 任务无需重启
crond 服务,它会每分钟自动重读。但修改 /etc/crontab 或 /etc/cron.d/ 下的文件后,某些旧版本可能需要等待几分钟才能感知。
4.2.2 常见错误排查
| 错误现象 |
原因分析 |
解决方案 |
| 任务不执行,cron日志无记录 |
crond 服务未运行,或用户被 cron.deny 禁止 |
systemctl status crond 检查服务;检查 /etc/cron.allow 和 /etc/cron.deny |
| 任务执行了但结果不对 |
环境变量不同,PATH 中找不到命令 |
脚本开头 source /etc/profile,或在 crontab 中定义 PATH |
命令中含 % 导致任务失败 |
% 在 crontab 中是特殊字符 |
用 \% 转义,或把命令写在脚本里 |
/var/spool/mail 文件暴涨 |
任务输出未重定向,通过邮件发送 |
所有任务加 >> /path/to/log 2>&1,或设置 MAILTO="" |
| 任务重复执行 |
执行时间超过调度间隔,多个实例并行 |
使用 flock 文件锁防重复 |
/etc/cron.d/ 下的任务不执行 |
文件权限不对,或文件名含“.” |
文件权限设为 644,文件名不要包含点号 |
| 脚本手动执行正常,cron 执行失败 |
环境变量差异或工作目录不同 |
用 env > /tmp/cron-env.txt 对比环境差异 |
4.2.3 兼容性问题
- CentOS vs Ubuntu 的 cron 差异:服务名(
crond vs cron)、日志路径(/var/log/cron vs /var/log/syslog)、crontab 存储路径均有不同,编写跨平台脚本时需注意。
- 不同 cron 实现的特性差异:
cronie 支持 @reboot、@yearly 等快捷写法,但一些老版本可能不支持。追求兼容性时,建议使用标准的5字段表达式。
- systemd timer 的版本差异:CentOS 7 的 systemd 219 不支持
RandomizedDelaySec 等参数,高级 OnCalendar 表达式也可能受限。CentOS 8 的 systemd 239 功能更完整。
五、故障排查和监控
5.1 故障排查
5.1.1 日志查看
# CentOS/RHEL - cron 专用日志
tail -f /var/log/cron
# Ubuntu/Debian - 从 syslog 中过滤
tail -f /var/log/syslog | grep CRON
# 用 journalctl 查看(通用)
journalctl -u crond --since "1 hour ago" # CentOS
journalctl -u cron --since "1 hour ago" # Ubuntu
# 查看指定用户的 cron 执行记录
grep "$(whoami)" /var/log/cron | tail -20 # CentOS
# 查看 cron 任务的邮件输出(如果没重定向的话)
cat /var/spool/mail/root | tail -50
# 查看邮件队列大小
du -sh /var/spool/mail/*
5.1.2 常见问题排查流程
问题一:cron 任务不执行
- 确认
crond/cron 服务正在运行 (systemctl status)。
- 确认当前用户的 crontab 中有任务 (
crontab -l)。
- 检查 cron 日志中是否有该任务的执行记录。
- 检查用户是否被
/etc/cron.allow 或 /etc/cron.deny 禁止。
- 使用在线工具(如 crontab.guru)验证时间表达式是否正确。
- 检查脚本文件是否有执行权限 (
chmod +x)。
问题二:任务执行了但结果不对(环境变量问题)
- 在 crontab 中添加临时任务导出环境:
* * * * * env > /tmp/cron-env.txt。
- 在登录 Shell 中导出环境:
env > /tmp/shell-env.txt。
- 使用
diff 命令对比两个文件,通常会发现 PATH、JAVA_HOME 等变量差异。
- 在脚本开头
source /etc/profile 和 source ~/.bashrc,或在 crontab 中明确定义变量。
问题三:邮件队列爆满
- 检查
/var/spool/mail/ 目录大小。
- 使用
crontab -l | grep -v '>' 找出没有重定向输出的任务。
- 为这些任务添加输出重定向 (
>> /path/to/log 2>&1)。
- 或在 crontab 开头设置
MAILTO="" 全局禁用邮件通知。
5.2 性能监控
可以编写监控脚本,定期收集以下关键指标,并集成到 Prometheus、Zabbix 等监控系统中:
crond 进程状态
crond 进程资源占用(正常应极低)
- 当前正在运行的 cron 任务
- 每日 cron 执行总次数和失败次数
/var/spool/mail 目录大小
- 锁文件持有状态和时间
六、总结与进阶
6.1 技术要点回顾
- 输出必须重定向:避免邮件队列爆满,标准写法是
>> /path/to/log 2>&1。
- 环境变量是头号“杀手”:通过脚本内
source、crontab 定义 PATH、使用绝对路径三重保险解决。
- 必须使用文件锁:使用
flock -xn /tmp/task.lock -c 'command' 防止任务重复执行导致数据损坏。
% 字符必须转义:date +%Y%m%d 在 crontab 中必须写成 date +\%Y\%m\%d。
- 拥抱现代方案:对于需要资源限制、秒级精度、自动日志的复杂任务,
systemd timer 是更好的选择。
- 定期备份 crontab:使用脚本自动备份所有用户及系统级的 crontab 配置,并考虑纳入版本管理。
6.2 进阶学习方向
当服务器规模扩大或任务复杂度增加时,可以考虑以下方向:
- 分布式任务调度系统:如 XXL-JOB、Airflow、Azkaban,提供 Web 界面、任务依赖、失败重试、统一视图等功能,适合大规模集群。
- 使用配置管理工具:如 Ansible、SaltStack,通过代码声明式地批量管理所有服务器的 crontab 配置,实现配置即代码。
- CI/CD 流水线中的定时触发:将与代码部署相关的定时任务(如定时测试、构建)迁移到 Jenkins、GitLab CI 等平台,使其与代码版本绑定。
Crontab 作为 Linux 系统 中最基础且强大的定时任务工具,深入理解其原理和避坑指南,是每一位运维工程师和开发者的必备技能。希望这篇来自云栈社区的指南能帮助你更安全、高效地管理定时任务。