名称: cron-scheduling
描述: 使用 cron 和 systemd 定时器来调度和管理周期性任务。适用于设置 cron 作业、编写 systemd 定时器单元、处理时区感知调度、监控失败作业、实现重试模式或调试计划任务未运行的原因。
元数据: {"clawdbot":{"emoji":"⏰","requires":{"anyBins":["crontab","systemctl","at"]},"os":["linux","darwin"]}}
调度和管理周期性任务。涵盖 cron 语法、crontab 管理、systemd 定时器、一次性调度、时区处理、监控和常见故障模式。
┌───────── 分钟 (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 -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
# 显式设置 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 "健康检查失败"
# /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
# 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"
Systemd 定时器 vs cron:
+ 日志记录在 journald 中(journalctl -u 服务名)
+ 持久性:重启后能补执行错过的运行
+ RandomizedDelaySec:防止“惊群效应”
+ 依赖关系:可以依赖网络、挂载点等
+ 资源限制:CPUQuota、MemoryMax 等
+ 无邮件丢失问题(MAILTO 常配置错误)
- 需要创建更多文件(服务 + 定时器)
- 配置更冗长
# 调度命令
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 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 定时器能正确处理夏令时
# 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/
>> /var/log/job.log 2>&1。否则,输出会发送到邮件(如果配置了)或静默丢失。env -i 运行 cron 作业来模拟 cron 的最小化环境进行测试。大多数失败是由缺少 PATH 或环境变量引起的。flock 防止重叠运行。systemd-analyze calendar 在部署前验证定时器调度非常有用。Persistent=true)和资源限制。