OA0 = Omni AI 0
OA0 是一个探索 AI 的论坛
现在注册
已注册用户请  登录
OA0  ›  技能包  ›  cron-scheduling:使用 cron 调度并管理周期性任务

cron-scheduling:使用 cron 调度并管理周期性任务

 
  proxy ·  2026-02-06 18:22:10 · 3 次点击  · 0 条评论  

名称: cron-scheduling
描述: 使用 cron 和 systemd 定时器来调度和管理周期性任务。适用于设置 cron 作业、编写 systemd 定时器单元、处理时区感知调度、监控失败作业、实现重试模式或调试计划任务未运行的原因。
元数据: {"clawdbot":{"emoji":"⏰","requires":{"anyBins":["crontab","systemctl","at"]},"os":["linux","darwin"]}}


Cron 与任务调度

调度和管理周期性任务。涵盖 cron 语法、crontab 管理、systemd 定时器、一次性调度、时区处理、监控和常见故障模式。

使用场景

  • 按计划运行脚本(备份、报告、清理)
  • 设置 systemd 定时器(现代 cron 替代方案)
  • 调试计划任务未运行的原因
  • 处理计划任务中的时区问题
  • 监控作业失败并发送告警
  • 运行一次性延迟命令

Cron 语法

五个字段

┌───────── 分钟 (0-59)
│ ┌─────── 小时 (0-23)
│ │ ┌───── 月份中的日期 (1-31)
│ │ │ ┌─── 月份 (1-12 或 JAN-DEC)
│ │ │ │ ┌─ 星期几 (0-7, 0 和 7 = 星期日,或 SUN-SAT)
│ │ │ │ │
* * * * * 命令

常用调度示例

# 每分钟
* * * * * /path/to/script.sh

# 每 5 分钟
*/5 * * * * /path/to/script.sh

# 每小时整点
0 * * * * /path/to/script.sh

# 每天凌晨 2:30
30 2 * * * /path/to/script.sh

# 每周一上午 9:00
0 9 * * 1 /path/to/script.sh

# 每个工作日(周一至周五)上午 8:00
0 8 * * 1-5 /path/to/script.sh

# 每月 1 日午夜
0 0 1 * * /path/to/script.sh

# 工作日(周一至周五 9-17 点)每 15 分钟
*/15 9-17 * * 1-5 /path/to/script.sh

# 每天两次(上午 9 点和下午 5 点)
0 9,17 * * * /path/to/script.sh

# 每季度(1月、4月、7月、10月)1 日午夜
0 0 1 1,4,7,10 * /path/to/script.sh

# 每周日凌晨 3 点
0 3 * * 0 /path/to/script.sh

特殊字符串(简写)

@reboot    /path/to/script.sh   # 系统启动时运行一次
@yearly    /path/to/script.sh   # 等同于 0 0 1 1 *
@monthly   /path/to/script.sh   # 等同于 0 0 1 * *
@weekly    /path/to/script.sh   # 等同于 0 0 * * 0
@daily     /path/to/script.sh   # 等同于 0 0 * * *
@hourly    /path/to/script.sh   # 等同于 0 * * * *

Crontab 管理

# 编辑当前用户的 crontab
crontab -e

# 列出当前 crontab
crontab -l

# 编辑其他用户的 crontab(需要 root 权限)
sudo crontab -u www-data -e

# 删除所有 cron 作业(谨慎操作!)
crontab -r

# 从文件安装 crontab
crontab mycrontab.txt

# 备份 crontab
crontab -l > crontab-backup-$(date +%Y%m%d).txt

Crontab 最佳实践

# 显式设置 PATH(cron 的 PATH 环境变量非常有限)
PATH=/usr/local/bin:/usr/bin:/bin

# 设置 MAILTO 以接收错误通知
MAILTO=admin@example.com

# 显式设置 shell
SHELL=/bin/bash

# 完整的 crontab 示例
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
SHELL=/bin/bash

# 备份任务
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# 清理旧日志
0 3 * * 0 find /var/log/myapp -name "*.log" -mtime +30 -delete

# 健康检查
*/5 * * * * /opt/scripts/healthcheck.sh || /opt/scripts/alert.sh "健康检查失败"

Systemd 定时器

创建定时器(现代 cron 替代方案)

# /etc/systemd/system/backup.service
[Unit]
Description=每日备份

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=backup
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/backup.timer
[Unit]
Description=每日凌晨 2 点运行备份

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
# 启用并启动定时器
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer

# 检查定时器状态
systemctl list-timers
systemctl list-timers --all

# 检查上次运行情况
systemctl status backup.service
journalctl -u backup.service --since today

# 手动运行(用于测试)
sudo systemctl start backup.service

# 禁用定时器
sudo systemctl disable --now backup.timer

OnCalendar 语法

# Systemd 日历表达式

# 每日午夜
OnCalendar=daily
# 或:OnCalendar=*-*-* 00:00:00

# 每周一上午 9 点
OnCalendar=Mon *-*-* 09:00:00

# 每 15 分钟
OnCalendar=*:0/15

# 工作日(周一至周五)上午 8 点
OnCalendar=Mon..Fri *-*-* 08:00:00

# 每月 1 日
OnCalendar=*-*-01 00:00:00

# 每 6 小时
OnCalendar=0/6:00:00

# 特定日期
OnCalendar=2026-02-03 12:00:00

# 测试日历表达式
systemd-analyze calendar "Mon *-*-* 09:00:00"
systemd-analyze calendar "*:0/15"
systemd-analyze calendar --iterations=5 "Mon..Fri *-*-* 08:00:00"

相对于 cron 的优势

Systemd 定时器 vs cron:
+ 日志记录在 journald 中(journalctl -u 服务名)
+ 持久性:重启后能补执行错过的运行
+ RandomizedDelaySec:防止“惊群效应”
+ 依赖关系:可以依赖网络、挂载点等
+ 资源限制:CPUQuota、MemoryMax 等
+ 无邮件丢失问题(MAILTO 常配置错误)
- 需要创建更多文件(服务 + 定时器)
- 配置更冗长

一次性调度

at 命令(在特定时间运行一次)

# 调度命令
echo "/opt/scripts/deploy.sh" | at 2:00 AM tomorrow
echo "reboot" | at now + 30 minutes
echo "/opt/scripts/report.sh" | at 5:00 PM Friday

# 交互式(输入命令,按 Ctrl+D 结束)
at 10:00 AM
> /opt/scripts/task.sh
> echo "完成" | mail -s "任务完成" admin@example.com
> <Ctrl+D>

# 列出待处理作业
atq

# 查看作业详情
at -c <作业编号>

# 删除作业
atrm <作业编号>

基于 sleep 的调度(最简单)

# 延迟一段时间后运行命令
(sleep 3600 && /opt/scripts/task.sh) &

# 使用 nohup(退出登录后仍运行)
nohup bash -c "sleep 7200 && /opt/scripts/task.sh" &

时区处理

# Cron 默认使用系统时区运行
# 检查系统时区
timedatectl
date +%Z

# 为特定 cron 作业设置时区
# 方法 1:在 crontab 中设置 TZ 变量
TZ=America/New_York
0 9 * * * /opt/scripts/report.sh

# 方法 2:在脚本内部设置
#!/bin/bash
export TZ=UTC
# 现在所有日期操作都使用 UTC

# 方法 3:包装器
TZ=Europe/London date '+%Y-%m-%d %H:%M:%S'

# 列出可用时区
timedatectl list-timezones
timedatectl list-timezones | grep America

夏令时陷阱

问题:在夏令时转换期间,计划在凌晨 2:30 运行的作业可能会运行两次或根本不运行。

"向前跳":凌晨 2:30 不存在(时钟从 2:00 跳到 3:00)
"向后跳":凌晨 2:30 出现两次

缓解措施:
1. 将关键作业安排在凌晨 1:00-3:00 之外
2. 调度时使用 UTC:在 crontab 中设置 TZ=UTC
3. 使作业具有幂等性(运行两次是安全的)
4. Systemd 定时器能正确处理夏令时

监控与调试

为什么我的 cron 作业没有运行?

# 1. 检查 cron 守护进程是否正在运行
systemctl status cron    # Debian/Ubuntu
systemctl status crond   # CentOS/RHEL

# 2. 检查 cron 日志
grep CRON /var/log/syslog           # Debian/Ubuntu
grep CRON /var/log/cron             # CentOS/RHEL
journalctl -u cron --since today    # systemd

# 3. 检查 crontab 是否实际存在
crontab -l

# 4. 手动测试命令(使用 cron 的环境)
env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /opt/scripts/backup.sh
# 如果此处失败但正常运行时成功 → PATH 或环境变量问题

# 5. 检查权限
ls -la /opt/scripts/backup.sh   # 必须可执行
ls -la /var/spool/cron/         # Crontab 文件权限

# 6. 检查 crontab 中的语法错误
# cron 会静默忽略有错误的行

# 7. 检查输出是否被丢弃
# 默认情况下,cron 会通过邮件发送输出。如果没有邮件传输代理,输出会丢失。
# 始终重定向输出:>> /var/log/myjob.log 2>&1

带有日志记录和告警的作业包装器

#!/bin/bash
# cron-wrapper.sh — 运行命令,附带日志记录、计时和错误告警
# 用法:cron-wrapper.sh <作业名称> <命令> [参数...]

set -euo pipefail

JOB_NAME="${1:?用法: cron-wrapper.sh <作业名称> <命令> [参数...]}"
shift
COMMAND=("$@")

LOG_DIR="/var/log/cron-jobs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/$JOB_NAME.log"

log() { echo "[$(date -u '+%Y-%m-%dT%H:%M:%SZ')] $*" >> "$LOG_FILE"; }

log "开始: ${COMMAND[*]}"
START_TIME=$(date +%s)

if "${COMMAND[@]}" >> "$LOG_FILE" 2>&1; then
    ELAPSED=$(( $(date +%s) - START_TIME ))
    log "成功 (耗时 ${ELAPSED}s)"
else
    EXIT_CODE=$?
    ELAPSED=$(( $(date +%s) - START_TIME ))
    log "失败,退出码 $EXIT_CODE (耗时 ${ELAPSED}s)"
    # 告警(根据需要自定义)
    echo "Cron 作业 '$JOB_NAME' 失败,退出码 $EXIT_CODE" | \
        mail -s "CRON 失败: $JOB_NAME" admin@example.com 2>/dev/null || true
    exit $EXIT_CODE
fi
# 在 crontab 中使用:
0 2 * * * /opt/scripts/cron-wrapper.sh daily-backup /opt/scripts/backup.sh
*/5 * * * * /opt/scripts/cron-wrapper.sh health-check /opt/scripts/healthcheck.sh

防止作业重叠的锁机制

# 防止并发运行(当作业执行时间可能超过调度间隔时)
# 方法 1:使用 flock
* * * * * flock -n /tmp/myjob.lock /opt/scripts/slow-job.sh

# 方法 2:在脚本内部实现
LOCKFILE="/tmp/myjob.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "已在运行"; exit 0; }
# ... 执行工作 ...

幂等作业模式

# 幂等备份(仅在上次备份之后才创建)
#!/bin/bash
BACKUP_DIR="/backups/$(date +%Y%m%d)"
[[ -d "$BACKUP_DIR" ]] && { echo "备份已存在"; exit 0; }
mkdir -p "$BACKUP_DIR"
pg_dump mydb > "$BACKUP_DIR/mydb.sql"

# 幂等清理(多次运行是安全的)
find /tmp/uploads -mtime +7 -type f -delete 2>/dev/null || true

# 幂等同步(rsync 仅传输变更部分)
rsync -az /data/ backup-server:/backups/data/

实用技巧

  • 始终在 cron 作业中重定向输出:>> /var/log/job.log 2>&1。否则,输出会发送到邮件(如果配置了)或静默丢失。
  • 使用 env -i 运行 cron 作业来模拟 cron 的最小化环境进行测试。大多数失败是由缺少 PATH 或环境变量引起的。
  • 当作业执行时间可能超过其调度间隔时,使用 flock 防止重叠运行。
  • 使所有计划任务具有幂等性。如果作业运行两次(夏令时、手动触发、崩溃恢复),应产生相同的结果。
  • systemd-analyze calendar 在部署前验证定时器调度非常有用。
  • 如果适用夏令时,切勿在凌晨 1:00 到 3:00 之间安排关键作业。请改用 UTC 调度。
  • 记录每个 cron 作业的开始时间、结束时间和退出码。没有这些信息,事后调试失败只能是猜测。
  • 对于生产服务,优先使用 systemd 定时器而非 cron:你可以免费获得 journald 日志记录、错过的运行补执行(Persistent=true)和资源限制。
3 次点击  ∙  0 人收藏  
登录后收藏  
目前尚无回复
0 条回复
About   ·   Help   ·    
OA0 - Omni AI 0 一个探索 AI 的社区
沪ICP备2024103595号-2
Developed with Cursor