引言
在 VPS 日常运维中,定时任务和后台作业几乎无处不在:
- 每天凌晨备份数据库
- 每小时检查磁盘空间
- 每周一早上清理临时文件
- 一次性执行迁移脚本
- 服务异常时自动重启
你用了哪套工具?Cron?Systemd Timers?还是全靠手动 nohup?
不同的调度场景需要不同的工具。Cron 简单直接但缺乏现代特性,Systemd Timers 功能强大但配置稍复杂,At 适合一次性任务。本文将全面对比这三种工具,帮你建立清晰的选择框架和实战能力。
一、Cron:经典定时任务之王
1.1 基本语法
Cron 的核心是一个文本文件(crontab),每行代表一个任务:
# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 日期 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 星期 (0 - 6, 0=周日)
# │ │ │ │ │
# * * * * * 要执行的命令
常用示例:
# 每天凌晨 2:00 备份数据库
0 2 * * * /usr/bin/mysqldump -u root mydb | gzip > /backup/db_$(date +\%Y\%m\%d).sql.gz
# 每 5 分钟检查磁盘
*/5 * * * * /usr/local/bin/check_disk.sh
# 每周一早上 9:00 清理日志
0 9 * * 1 /usr/bin/find /var/log -mtime +30 -delete
# 每月 1 号凌晨 3:30 清理缓存
30 3 1 * * /usr/bin/clear_cache.sh
# 每天早上 8:00 和晚上 8:00 同步时间
0 8,20 * * * /usr/sbin/ntpdate pool.ntp.org
1.2 管理命令
# 编辑当前用户的 crontab
crontab -e
# 查看当前 crontab
crontab -l
# 删除当前 crontab
crontab -r
# 查看系统级 cron 任务
sudo cat /etc/crontab
# 查看系统 cron 目录下的任务
ls /etc/cron.d/
ls /etc/cron.daily/
ls /etc/cron.hourly/
ls /etc/cron.weekly/
ls /etc/cron.monthly/
1.3 环境变量问题
Cron 的环境变量非常有限,这是新手最常见的问题之一:
# 在 crontab 中明确设置环境变量
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=admin@example.com
# 每天凌晨 2 点备份
0 2 * * * /usr/bin/mysqldump -u root mydb > /backup/db.sql
重要提示:Cron 默认使用 /bin/sh,不是 /bin/bash。如果你的脚本用了 bash 特性(数组、[[ ]] 等),务必在 crontab 顶部指定 SHELL=/bin/bash。
1.4 Cron 的局限性
| 问题 | 说明 |
|---|---|
| 无任务依赖管理 | 无法定义"任务 A 完成后执行任务 B" |
| 无任务优先级 | 所有任务平等,无法设置优先级 |
| 错误处理弱 | 失败后只能发邮件或写日志,无自动重试 |
| 无任务互斥 | 同一任务并发执行可能导致数据混乱 |
| 调试困难 | 没有结构化日志,排查问题时只能 grep |
| 资源限制弱 | 无法精确控制 CPU/内存使用上限 |
二、Systemd Timers:现代 Linux 的任务调度器
2.1 核心优势
Systemd Timers 是 Systemd 提供的定时任务机制,相比 Cron 有以下优势:
- 服务管理一体化:Timer 和 Service 绑定,方便启停和监控
- 精准调度:支持
OnCalendar(日历触发)和OnBootSec/OnUnitActiveSec(相对时间触发) - 资源限制:可直接在 Service 中设置 CPU/内存限制
- 依赖管理:可定义前置依赖和后置动作
- 日志集成:输出自动进入 journal,
journalctl一键查看 - 任务互斥:默认防止并发执行(
Persist=true) - 失败重试:可配置重试策略
2.2 创建一个 Timer 任务
假设我们要每天凌晨 2 点执行数据库备份:
步骤 1:创建 Service 单元文件
# /etc/systemd/system/db-backup.service
[Unit]
Description=Daily Database Backup
After=network.target mysql.service
[Service]
Type=oneshot
User=postgres
WorkingDirectory=/opt/backup
Environment="PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/usr/bin/pg_dump mydb | gzip > /opt/backup/db_$(date +\%Y\%m\%d).sql.gz
StandardOutput=journal
StandardError=journal
# 资源限制
MemoryMax=512M
CPUQuota=50%
# 失败重试
Restart=on-failure
RestartSec=60
StartLimitIntervalSec=600
StartLimitBurst=3
步骤 2:创建 Timer 单元文件
# /etc/systemd/system/db-backup.timer
[Unit]
Description=Daily Database Backup Timer
[Timer]
# 每天凌晨 2:00 触发
OnCalendar=*-*-* 02:00:00
# 延迟最多 5 分钟(避免大量任务同时启动)
RandomizedDelaySec=5min
# 即使机器休眠,醒来后也会补跑
Persistent=true
# 开机后 5 分钟也运行一次(确保不会遗漏)
OnBootSec=5min
[Install]
WantedBy=timers.target
步骤 3:启用并启动
# 重新加载 systemd 配置
sudo systemctl daemon-reload
# 启用 Timer(开机自启)
sudo systemctl enable db-backup.timer
# 启动 Timer
sudo systemctl start db-backup.timer
# 查看 Timer 状态
sudo systemctl status db-backup.timer
# 查看下次触发时间
sudo systemctl list-timers --all
2.3 常用 Timer 表达式
# ── 绝对时间(OnCalendar)──
# 每分钟
OnCalendar=*:0/1
# 每 5 分钟
OnCalendar=*:0/5
# 每小时第 15 分钟
OnCalendar=*-*-* *:15:00
# 每天凌晨 2:30
OnCalendar=*-*-* 02:30:00
# 每周一早上 9:00
OnCalendar=Mon *-*-* 09:00:00
# 每月 1 号凌晨 3:00
OnCalendar=01 *-* 03:00:00
# 每季度第一天
OnCalendar=*-01,04,07,10-01 00:00:00
# 每周六凌晨
OnCalendar=Sat *-*-* 00:00:00
# 2026 年内的每个工作日
OnCalendar=Mon..Fri *-*-* 09:00:00
# ── 相对时间(启动/激活后)──
# 启动后 5 分钟
OnBootSec=5min
# 上次激活后 1 小时
OnUnitActiveSec=1h
# 上次停止后 30 分钟
OnUnitInactiveSec=30min
# 启动后 2 小时,且上次激活后 30 分钟(取较晚者)
AccuracySec=1us
2.4 Systemd Timer 的最佳实践
# 查看 Timer 详细状态
systemctl list-timers --all --no-pager
# 查看某 Timer 的历史触发记录
journalctl -u db-backup.timer --since "24 hours ago"
# 查看 Timer 执行的 Service 日志
journalctl -u db-backup.service --since "24 hours ago"
# 手动触发一次(测试用)
systemctl start db-backup.service
# 查看 Timer 倒计时
systemctl show db-backup.timer --property=NextElapseUSecRealtime
三、At:一次性定时任务
3.1 适用场景
At 专门用于一次性定时任务,不适合周期性调度:
- 半小时后发送提醒邮件
- 今晚 11 点清理临时文件
- 明天早上重启某个服务
- 系统维护窗口前自动暂停某服务
3.2 基本用法
# 查看待执行的 at 任务
atq
# 删除某个 at 任务
atrm <job-id>
# 今晚 23:00 执行清理
echo "/usr/bin/find /tmp -mtime +1 -delete" | at 23:00
# 明天早上 8:00 执行
echo "systemctl restart nginx" | at 8:00 tomorrow
# 30 分钟后执行
echo "/usr/local/bin/backup.sh" | at now + 30 minutes
# 下周一早上 9:00 执行
echo "run_migration.sh" | at 9:00 Monday
# 2026 年 12 月 31 日 23:59 执行
echo "countdown.sh" | at 23:59 12/31/2026
3.3 注意事项
- At 任务在指定时间执行一次后自动删除,不会重复
- 如果目标时间已过,任务会在当天结束时执行(可加
now + N minutes避免) - At 默认发送执行结果到用户邮箱,可在命令中用
> /dev/null 2>&1屏蔽 - 需要确保
atd服务正在运行
# 检查 atd 服务状态
sudo systemctl status atd
# 启动 atd 服务
sudo systemctl start atd
sudo systemctl enable atd
四、三种工具横向对比
| 特性 | Cron | Systemd Timers | At |
|---|---|---|---|
| 调度类型 | 周期任务 | 周期任务 | 一次性任务 |
| 精度 | 分钟级 | 微秒级 | 分钟级 |
| 日志管理 | 邮件/手动 | journalctl | 邮件 |
| 资源控制 | 无 | 强(Memory/CPU/I/O) | 无 |
| 并发控制 | 无 | 有(防止重叠) | 无 |
| 失败重试 | 无 | 有 | 无 |
| 依赖管理 | 无 | 有 | 无 |
| 学习曲线 | 低 | 中 | 低 |
| 适用场景 | 简单周期任务 | 复杂/关键任务 | 一次性任务 |
五、实战场景组合
5.1 场景一:数据库每日备份
# /etc/systemd/system/db-backup.timer
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
RandomizedDelaySec=5min
# /etc/systemd/system/db-backup.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/db-backup.sh
MemoryMax=1G
CPUQuota=80%
5.2 场景二:每小时健康检查
# /etc/systemd/system/health-check.timer
[Timer]
OnCalendar=*:0/60
Persistent=true
# /etc/systemd/system/health-check.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/health-check.sh
StandardOutput=journal
StandardError=journal
5.3 场景三:维护窗口一次性操作
# 今晚 3:00 重启服务并清理缓存
echo "systemctl restart app && rm -rf /tmp/*" | at 3:00 tomorrow
5.4 场景四:多级依赖任务链
# /etc/systemd/system/backup-chain.timer
[Timer]
OnCalendar=*-*-* 01:00:00
Persistent=true
# /etc/systemd/system/backup-chain.service
[Unit]
Requires=db-backup.service
After=db-backup.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup-chain.sh
六、常见问题排查
6.1 Cron 任务不执行
# 检查 cron 服务是否运行
sudo systemctl status cron
# 检查任务是否有语法错误
sudo crontab -l
# 查看 cron 日志
sudo grep CRON /var/log/syslog
sudo journalctl -u cron --since "24 hours ago"
# 常见坑:路径问题
# Cron 的 PATH 很有限,务必用绝对路径
/usr/bin/python3 /opt/scripts/task.py
6.2 Systemd Timer 不触发
# 检查 Timer 状态
systemctl status db-backup.timer
# 查看下次触发时间
systemctl show db-backup.timer --property=NextElapseUSecRealtime
# 检查 Service 是否有语法错误
systemd-analyze verify db-backup.service
# 查看完整日志
journalctl -u db-backup.timer -u db-backup.service --since "1 hour ago"
6.3 At 任务未执行
# 检查 atd 服务
sudo systemctl status atd
# 查看待执行队列
atq
# 查看已执行历史
grep at-agent /var/log/syslog
七、高级技巧
7.1 Cron 与 Systemd 混合使用
对于大多数场景,建议:
- 简单周期任务 → Cron
- 关键业务任务 → Systemd Timers
- 一次性维护任务 → At
7.2 使用 fswatch 替代轮询
与其用 Cron 每分钟检查文件变化,不如用 inotify 监听:
# 安装 fswatch
sudo apt install fswatch
# 监听文件变化并触发脚本
fswatch -o /var/www/html | while read; do
/opt/scripts/deploy.sh
done
7.3 任务执行超时控制
# Cron 中使用 timeout 命令
*/5 * * * * timeout 300 /usr/local/bin/long-running-task.sh
# Systemd 中设置超时
[Service]
TimeoutStartSec=300
TimeoutStopSec=60
7.4 并发控制:防止任务重叠
# Cron 方式:使用 flock
*/5 * * * * flock -n /tmp/mytask.lock /usr/local/bin/mytask.sh
# Systemd 方式:内置防重叠
[Timer]
Persistent=true
# Systemd 默认防止同一 unit 并发执行
总结
| 场景 | 推荐工具 |
|---|---|
| 简单的周期性备份/清理 | Cron |
| 关键的、需要资源限制的任务 | Systemd Timers |
| 一次性维护操作 | At |
| 需要依赖链的复杂流程 | Systemd Timers |
| 高频检查(分钟级以下) | Systemd Timers |
核心原则:简单任务用 Cron,关键任务用 Systemd Timers,一次性任务用 At。根据任务的重要性、复杂度和调度需求,灵活组合三种工具,构建稳健的 VPS 自动化运维体系。
