找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3687

积分

0

好友

507

主题
发表于 2026-2-14 02:17:28 | 查看: 29| 回复: 0

一、概述

1.1 背景介绍

Crontab 是 Linux 系统下最经典的定时任务工具,但它也是线上事故的“高发区”。环境变量丢失、任务重复执行、输出未重定向导致邮件队列爆满——这些坑,几乎每个运维工程师都踩过。我们团队就曾处理过一起线上事故:一个备份脚本没有加文件锁,凌晨2点触发后因为执行时间超过了1小时,第二次调度又启动了一个新实例,两个进程同时写入同一个备份文件,最终导致备份数据损坏。自那以后,团队立下规矩:所有 Cron 任务必须加锁、必须重定向输出、必须记录执行日志。

Cron 本身的机制并不复杂:crond 守护进程每分钟醒来一次,检查所有 crontab 配置,将到时间的任务拉起执行。但简单不代表不会出问题,恰恰是因为太简单,很多细节容易被忽略。比如,Cron 的执行环境和你通过 SSH 登录后的 Shell 环境完全不同,其 PATH 通常只有 /usr/bin:/bin,很多自定义命令会找不到;再比如,Cron 任务的 stdoutstderr 默认会通过邮件发送给用户,如果不做处理,/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 主要有三个位置:

  1. /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
  2. /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
  3. /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 里定义 PATHSHELL,脚本里也 source /etc/profile 做兜底,命令一律用绝对路径。三重保险,基本杜绝了环境变量问题。

2.2.5 输出重定向

Cron 任务的 stdoutstderr 默认会通过邮件发送给 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 文件系统代替外部命令)来降低开销。
  • 使用 ionicenice 降低任务优先级:对于备份、归档等非紧急任务,可以降低其 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 安全加固

  • 限制 cron 使用权限:通过 /etc/cron.allow/etc/cron.deny 文件控制哪些用户可以使用 crontab 命令。
    # 只允许 root 和 deploy 用户使用 cron
    echo "root" > /etc/cron.allow
    echo "deploy" >> /etc/cron.allow
    # cron.allow 存在时,只有列在里面的用户才能用 crontab
    chmod 600 /etc/cron.allow
  • Cron 任务脚本权限控制:脚本文件权限不应过松,防止被非授权用户篡改。建议设置为 750,属主为 root
  • 敏感信息不要写在 crontab 里:数据库密码、API Key 等不要直接写在命令行中,因为 crontab -l 任何同用户都能看到。应将敏感信息放在权限为 600 的配置文件中,由脚本读取。

4.1.3 高可用配置

  • Crontab 版本管理和备份:Crontab 没有版本历史,误删难以恢复。建议定期导出备份,并纳入 Git 等版本控制系统管理。
  • 关键任务双机部署:对于不能中断的定时任务(如监控告警),可在主备两台机器上都配置,通过分布式锁(Redis/etcd)或简单的主备检测机制(如检测哪个机器持有 VIP)来避免重复执行。
  • Crontab 变更审计:通过 Shell 函数包装或审计工具,记录谁在什么时候修改了 crontab,便于问题追溯。

4.2 注意事项

4.2.1 配置注意事项

  1. % 字符必须转义:在 crontab 中,% 是特殊字符,表示换行。命令中用到 % 必须用 \% 转义,最常见于 date 命令。
  2. 慎用 crontab -r:该命令会直接删除当前用户的所有 crontab 任务,没有确认提示。执行前务必先使用 crontab -l > /tmp/crontab-backup.txt 备份。
  3. 注意工作目录:Cron 任务的工作目录是用户的 HOME 目录,而非脚本所在目录。脚本中所有文件路径都应使用绝对路径。
  4. 配置生效时间:新添加的用户级 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 任务不执行

  1. 确认 crond/cron 服务正在运行 (systemctl status)。
  2. 确认当前用户的 crontab 中有任务 (crontab -l)。
  3. 检查 cron 日志中是否有该任务的执行记录。
  4. 检查用户是否被 /etc/cron.allow/etc/cron.deny 禁止。
  5. 使用在线工具(如 crontab.guru)验证时间表达式是否正确。
  6. 检查脚本文件是否有执行权限 (chmod +x)。

问题二:任务执行了但结果不对(环境变量问题)

  1. 在 crontab 中添加临时任务导出环境:* * * * * env > /tmp/cron-env.txt
  2. 在登录 Shell 中导出环境:env > /tmp/shell-env.txt
  3. 使用 diff 命令对比两个文件,通常会发现 PATHJAVA_HOME 等变量差异。
  4. 在脚本开头 source /etc/profilesource ~/.bashrc,或在 crontab 中明确定义变量。

问题三:邮件队列爆满

  1. 检查 /var/spool/mail/ 目录大小。
  2. 使用 crontab -l | grep -v '>' 找出没有重定向输出的任务。
  3. 为这些任务添加输出重定向 (>> /path/to/log 2>&1)。
  4. 或在 crontab 开头设置 MAILTO="" 全局禁用邮件通知。

5.2 性能监控

可以编写监控脚本,定期收集以下关键指标,并集成到 Prometheus、Zabbix 等监控系统中:

  • crond 进程状态
  • crond 进程资源占用(正常应极低)
  • 当前正在运行的 cron 任务
  • 每日 cron 执行总次数和失败次数
  • /var/spool/mail 目录大小
  • 锁文件持有状态和时间

六、总结与进阶

6.1 技术要点回顾

  1. 输出必须重定向:避免邮件队列爆满,标准写法是 >> /path/to/log 2>&1
  2. 环境变量是头号“杀手”:通过脚本内 source、crontab 定义 PATH、使用绝对路径三重保险解决。
  3. 必须使用文件锁:使用 flock -xn /tmp/task.lock -c 'command' 防止任务重复执行导致数据损坏。
  4. % 字符必须转义date +%Y%m%d 在 crontab 中必须写成 date +\%Y\%m\%d
  5. 拥抱现代方案:对于需要资源限制、秒级精度、自动日志的复杂任务,systemd timer 是更好的选择。
  6. 定期备份 crontab:使用脚本自动备份所有用户及系统级的 crontab 配置,并考虑纳入版本管理。

6.2 进阶学习方向

当服务器规模扩大或任务复杂度增加时,可以考虑以下方向:

  1. 分布式任务调度系统:如 XXL-JOB、Airflow、Azkaban,提供 Web 界面、任务依赖、失败重试、统一视图等功能,适合大规模集群。
  2. 使用配置管理工具:如 Ansible、SaltStack,通过代码声明式地批量管理所有服务器的 crontab 配置,实现配置即代码。
  3. CI/CD 流水线中的定时触发:将与代码部署相关的定时任务(如定时测试、构建)迁移到 Jenkins、GitLab CI 等平台,使其与代码版本绑定。

Crontab 作为 Linux 系统 中最基础且强大的定时任务工具,深入理解其原理和避坑指南,是每一位运维工程师和开发者的必备技能。希望这篇来自云栈社区的指南能帮助你更安全、高效地管理定时任务。





上一篇:动力电池管理系统(BMS)的核心技术解析:建模、状态估计与热管理策略
下一篇:基于换手率优化与标准化处理的量价关系因子构建与IC分析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 11:47 , Processed in 0.756521 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表