介绍
# Cron & Scheduling
调度和管理周期性任务。涵盖 cron 语法、crontab 管理、systemd 计时器、一次性调度、时区处理、监控以及常见故障模式。
## 使用场景
- 按计划运行脚本(备份、报告、清理) - 设置 systemd 计时器(cron 的现代替代方案) - 调试计划任务为何未运行 - 处理计划任务中的时区问题 - 监控作业失败并发送警报 - 运行一次性延迟命令
## Cron 语法
### 五个字段
``` ┌───────── minute (0-59) │ ┌─────── hour (0-23) │ │ ┌───── day of month (1-31) │ │ │ ┌─── month (1-12 or JAN-DEC) │ │ │ │ ┌─ day of week (0-7, 0 and 7 = Sunday, or SUN-SAT) │ │ │ │ │ * * * * * command ```
### 常见计划
```bash # Every minute * * * * * /path/to/script.sh
# Every 5 minutes */5 * * * * /path/to/script.sh
# Every hour at :00 0 * * * * /path/to/script.sh
# Every day at 2:30 AM 30 2 * * * /path/to/script.sh
# Every Monday at 9:00 AM 0 9 * * 1 /path/to/script.sh
# Every weekday at 8:00 AM 0 8 * * 1-5 /path/to/script.sh
# First day of every month at midnight 0 0 1 * * /path/to/script.sh
# Every 15 minutes during business hours (Mon-Fri 9-17) */15 9-17 * * 1-5 /path/to/script.sh
# Twice a day (9 AM and 5 PM) 0 9,17 * * * /path/to/script.sh
# Every quarter (Jan, Apr, Jul, Oct) on the 1st at midnight 0 0 1 1,4,7,10 * /path/to/script.sh
# Every Sunday at 3 AM 0 3 * * 0 /path/to/script.sh ```
### 特殊字符串(简写)
```bash @reboot /path/to/script.sh # Run once at startup @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 管理
```bash # Edit current user's crontab crontab -e
# List current crontab crontab -l
# Edit another user's crontab (root) sudo crontab -u www-data -e
# Remove all cron jobs (be careful!) crontab -r
# Install crontab from file crontab mycrontab.txt
# Backup crontab crontab -l > crontab-backup-$(date +%Y%m%d).txt ```
### Crontab 最佳实践
```bash # Set PATH explicitly (cron has minimal PATH) PATH=/usr/local/bin:/usr/bin:/bin
# Set MAILTO for error notifications [email protected]
# Set shell explicitly SHELL=/bin/bash
# Full crontab example PATH=/usr/local/bin:/usr/bin:/bin [email protected] SHELL=/bin/bash
# Backups 0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# Cleanup old logs 0 3 * * 0 find /var/log/myapp -name "*.log" -mtime +30 -delete
# Health check */5 * * * * /opt/scripts/healthcheck.sh || /opt/scripts/alert.sh "Health check failed" ```
## Systemd 计时器
### 创建计时器(cron 的现代替代品)
```ini # /etc/systemd/system/backup.service [Unit] Description=Daily backup
[Service] Type=oneshot ExecStart=/opt/scripts/backup.sh User=backup StandardOutput=journal StandardError=journal ```
```ini # /etc/systemd/system/backup.timer [Unit] Description=Run backup daily at 2 AM
[Timer] OnCalendar=*-*-* 02:00:00 Persistent=true RandomizedDelaySec=300
[Install] WantedBy=timers.target ```
```bash # Enable and start the timer sudo systemctl daemon-reload sudo systemctl enable --now backup.timer
# Check timer status systemctl list-timers systemctl list-timers --all
# Check last run systemctl status backup.service journalctl -u backup.service --since today
# Run manually (for testing) sudo systemctl start backup.service
# Disable timer sudo systemctl disable --now backup.timer ```
### OnCalendar 语法
```ini # Systemd calendar expressions
# Daily at midnight OnCalendar=daily # or: OnCalendar=*-*-* 00:00:00
# Every Monday at 9 AM OnCalendar=Mon *-*-* 09:00:00
# Every 15 minutes OnCalendar=*:0/15
# Weekdays at 8 AM OnCalendar=Mon..Fri *-*-* 08:00:00
# First of every month OnCalendar=*-*-01 00:00:00
# Every 6 hours OnCalendar=0/6:00:00
# Specific dates OnCalendar=2026-02-03 12:00:00
# Test calendar expressions systemd-analyze calendar "Mon *-*-* 09:00:00" systemd-analyze calendar "*:0/15" systemd-analyze calendar --iterations=5 "Mon..Fri *-*-* 08:00:00" ```
### 优于 cron 的优势
``` Systemd timers vs cron: + Logs in journald (journalctl -u service-name) + Persistent: catches up on missed runs after reboot + RandomizedDelaySec: prevents thundering herd + Dependencies: can depend on network, mounts, etc. + Resource limits: CPUQuota, MemoryMax, etc. + No lost-email problem (MAILTO often misconfigured) - More files to create (service + timer) - More verbose configuration ```
## 一次性调度
### at(在指定时间运行一次)
```bash # Schedule a command 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
# Interactive (type commands, Ctrl+D to finish) at 10:00 AM > /opt/scripts/task.sh > echo "Done" | mail -s "Task complete" [email protected] > <Ctrl+D>
# List pending jobs atq
# View job details at -c <job-number>
# Remove a job atrm <job-number> ```
### 基于 sleep(最简单)
```bash # Run something after a delay (sleep 3600 && /opt/scripts/task.sh) &
# With nohup (survives logout) nohup bash -c "sleep 7200 && /opt/scripts/task.sh" & ```
## 时区处理
```bash # Cron runs in the system timezone by default # Check system timezone timedatectl date +%Z
# Set timezone for a specific cron job # Method 1: TZ variable in crontab TZ=America/New_York 0 9 * * * /opt/scripts/report.sh
# Method 2: In the script itself #!/bin/bash export TZ=UTC # All date operations now use UTC
# Method 3: Wrapper TZ=Europe/London date '+%Y-%m-%d %H:%M:%S'
# List available timezones timedatectl list-timezones timedatectl list-timezones | grep America ```
### 夏令时 (DST) 陷阱
``` Problem: A job scheduled for 2:30 AM may run twice or not at all during DST transitions.
"Spring forward": 2:30 AM doesn't exist (clock jumps 2:00 → 3:00) "Fall back": 2:30 AM happens twice
Mitigation: 1. Schedule critical jobs outside 1:00-3:00 AM 2. Use UTC for the schedule: TZ=UTC in crontab 3. Make jobs idempotent (safe to run twice) 4. Systemd timers handle DST correctly ```
## 监控与调试
### 我的 cron 作业为什么没运行?
```bash # 1. Check cron daemon is running systemctl status cron # Debian/Ubuntu systemctl status crond # CentOS/RHEL
# 2. Check cron logs grep CRON /var/log/syslog # Debian/Ubuntu grep CRON /var/log/cron # CentOS/RHEL journalctl -u cron --since today # systemd
# 3. Check crontab actually exists crontab -l
# 4. Test the command manually (with cron's environment) env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /opt/scripts/backup.sh # If it fails here but works normally → PATH or env issue
# 5. Check permissions ls -la /opt/scripts/backup.sh # Must be executable ls -la /var/spool/cron/ # Crontab file permissions
# 6. Check for syntax errors in crontab # cron silently ignores lines with errors
# 7. Check if output is being discarded # By default, cron emails output. If no MTA, output is lost. # Always redirect: >> /var/log/myjob.log 2>&1 ```
### 带有日志记录和警报的作业封装器
```bash #!/bin/bash # cron-wrapper.sh — Run a command with logging, timing, and error alerting # Usage: cron-wrapper.sh <job-name> <command> [args...]
set -euo pipefail
JOB_NAME="${1:?Usage: cron-wrapper.sh <job-name> <command> [args...]}" 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 "START: ${COMMAND[*]}" START_TIME=$(date +%s)
if "${COMMAND[@]}" >> "$LOG_FILE" 2>&1; then ELAPSED=$(( $(date +%s) - START_TIME )) log "SUCCESS (${ELAPSED}s)" else EXIT_CODE=$? ELAPSED=$(( $(date +%s) - START_TIME )) log "FAILED with exit code $EXIT_CODE (${ELAPSED}s)" # Alert (customize as needed) echo "Cron job '$JOB_NAME' failed with exit $EXIT_CODE" | \ mail -s "CRON FAIL: $JOB_NAME" [email protected] 2>/dev/null || true exit $EXIT_CODE fi ```
```bash # Use in 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 ```
### 锁定以防止重叠
```bash # Prevent concurrent runs (job takes longer than interval) # Method 1: flock * * * * * flock -n /tmp/myjob.lock /opt/scripts/slow-job.sh
# Method 2: In the script LOCKFILE="/tmp/myjob.lock" exec 200>"$LOCKFILE" flock -n 200 || { echo "Already running"; exit 0; } # ... do work ... ```
## 幂等作业模式
```bash # Idempotent backup (only creates if newer than last backup) #!/bin/bash BACKUP_DIR="/backups/$(date +%Y%m%d)" [[ -d "$BACKUP_DIR" ]] && { echo "Backup already exists"; exit 0; } mkdir -p "$BACKUP_DIR" pg_dump mydb > "$BACKUP_DIR/mydb.sql"
# Idempotent cleanup (safe to run multiple times) find /tmp/uploads -mtime +7 -type f -delete 2>/dev/null || true
# Idempotent sync (rsync only transfers changes) 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`) 以及资源限制。