Linux 定时任务:Cron 与 Systemd Timer 完整指南¶
前言¶
定时任务是自动化运维的基石。备份数据库、清理过期日志、定期巡检服务——这些都不应该靠人工手动执行。Linux 提供了两个主流方案:经典的 Cron 和现代的 Systemd Timer。
本文从 Cron 语法开始,深入常见陷阱(环境变量、路径、日志),再对比 Systemd Timer 的优势,帮你选择最适合的自动化方式。
先读完这篇文章再写定时任务
太多人写过「脚本手动跑 OK,Cron 里就是不行」的 Bug。看完「常见陷阱」那一节,你会省下大量调试时间。
Cron 基础¶
Crontab 语法¶
* * * * * /path/to/command
│ │ │ │ │
│ │ │ │ └── 星期(0-7,0 和 7 都表示周日)
│ │ │ └────── 月份(1-12)
│ │ └──────────── 日期(1-31)
│ └────────────────── 小时(0-23)
└──────────────────────── 分钟(0-59)
常见时间表达式¶
| 表达式 | 含义 |
|---|---|
*/5 * * * * | 每 5 分钟 |
0 * * * * | 每小时整点 |
0 2 * * * | 每天凌晨 2 点 |
0 3 * * 0 | 每周日凌晨 3 点 |
0 4 1 * * | 每月 1 号凌晨 4 点 |
0 9-17 * * 1-5 | 工作日 9 点到 17 点,每小时一次 |
30 2 1,15 * * | 每月 1 号和 15 号凌晨 2:30 |
管理 Crontab¶
# 编辑当前用户的 crontab
crontab -e
# 查看当前用户的 crontab
crontab -l
# 删除当前用户的 crontab
crontab -r
# 以指定用户身份编辑(root 权限)
sudo crontab -u www-data -e
# 系统级 crontab(不推荐直接编辑)
cat /etc/crontab
Crontab 示例¶
# 每天凌晨 2 点备份数据库
0 2 * * * /opt/scripts/backup-db.sh
# 每小时清理超过 7 天的临时文件
0 * * * * find /tmp -name "*.tmp" -mtime +7 -delete
# 每 5 分钟检查服务健康
*/5 * * * * /opt/scripts/healthcheck.sh >> /var/log/healthcheck.log 2>&1
# 每周日 3 点更新软件包
0 3 * * 0 apt update && apt upgrade -y
Cron 常见陷阱¶
1. 环境变量不全¶
Cron 运行的环境比交互式 Shell 少得多——PATH 通常只有 /usr/bin:/bin,HOME、USER 以外的变量大多不存在。
# 错误示范:直接写 mysql 命令
0 2 * * * mysqldump -u root mydb > /backup/db.sql
# Cron: mysqldump: command not found(PATH 里没有)
# 正确做法 1:使用绝对路径
0 2 * * * /usr/bin/mysqldump -u root mydb > /backup/db.sql
# 正确做法 2:在脚本中设置 PATH
#!/bin/bash
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
mysqldump -u root mydb > /backup/db.sql
# 正确做法 3:在 crontab 顶部定义环境变量
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * mysqldump -u root mydb > /backup/db.sql
2. % 符号被转义¶
Cron 中 % 会被转换成换行符。如果你需要用 %(例如 date +%Y%m%d):
# 错误
0 2 * * * mysqldump mydb > /backup/db_$(date +%Y%m%d).sql
# 实际执行变成了:date +Y
# 正确:用反斜杠转义
0 2 * * * mysqldump mydb > /backup/db_$(date +\%Y\%m\%d).sql
# 或者把命令写到脚本里(推荐)
3. 输出无处可去¶
Cron 会把 stdout 和 stderr 通过邮件发送给用户。如果没配邮件服务,输出就丢了,错误也没法排查:
4. 凌晨 2 点到 3 点之间的夏令时坑¶
如果系统有时区切换(夏令时),凌晨 2:00 的任务可能: - 被跳过(时钟直接从 1:59 → 3:00) - 被执行两次(时钟回拨)
解决:避开凌晨 2:00-3:00,或改用 Systemd Timer(它用 UTC 单调时钟)。
Anacron:确保错过也能执行¶
如果服务器在凌晨 2 点关机了,Cron 错过了备份任务——第二天它不会补执行。anacron 弥补了这个缺陷:
# /etc/anacrontab
# 周期(天) 延迟(分钟) 任务名 命令
1 5 cron.daily run-parts /etc/cron.daily
7 10 cron.weekly run-parts /etc/cron.weekly
30 15 cron.monthly run-parts /etc/cron.monthly
Anacron 会记录上次执行时间,开机后检查是否有任务过期未执行,自动补跑。适用于非 7×24 运行的机器。
Systemd Timer 替代方案¶
Systemd Timer 是 Cron 的现代替代品,优势明显:
Timer 示例¶
/etc/systemd/system/cleanup.service:
[Unit]
Description=Temp File Cleanup
[Service]
Type=oneshot
ExecStart=/usr/bin/find /tmp -name "*.tmp" -mtime +7 -delete
/etc/systemd/system/cleanup.timer:
[Unit]
Description=Run temp cleanup hourly
[Timer]
OnCalendar=hourly
Persistent=true # 错过了补执行
RandomizedDelaySec=30 # 随机延迟 0~30 秒,避免惊群
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable cleanup.timer --now
# 查看所有 Timer
systemctl list-timers
高级 Timer 配置¶
[Timer]
# 按日历时间
OnCalendar=*-*-* 02:00:00 # 每天凌晨 2 点
OnCalendar=Mon..Fri 09:00 # 工作日 9 点
OnCalendar=Sat,Sun 06:00 # 周末 6 点
# 按间隔(单调时钟,不受夏令时影响)
OnUnitActiveSec=1h # 上次执行后 1 小时
OnBootSec=5min # 开机后 5 分钟
# 精度窗口(分散负载,避免惊群)
RandomizedDelaySec=60
# 固定间隔(不受任务执行时长影响,类似 Cron)
AccuracySec=1s
# 持久化(错过补执行)
Persistent=true
Cron vs Systemd Timer¶
| Cron | Systemd Timer | |
|---|---|---|
| 配置方式 | 单行命令 | Service + Timer 两个文件 |
| 日志 | 需自行重定向或依赖邮件 | journalctl -u xxx.service 自动记录 |
| 错过补执行 | 需 anacron 辅助 | Persistent=true |
| 随机延迟 | 手动 sleep $((RANDOM%60)) | RandomizedDelaySec= |
| 依赖管理 | 无 | After=、Requires= |
| 资源限制 | 无 | MemoryMax=、CPUQuota= |
| 夏令时 | 容易出坑 | OnUnitActiveSec 用单调时钟不受影响 |
| 调试 | 依赖邮件或手动执行 | systemctl status + journal 一体化 |
| 禁用 | 注释掉或删除行 | systemctl disable --now |
迁移建议
- 简单、低频的定时任务(每天备份一次):Cron 足够
- 需要日志、依赖、资源限制的:Systemd Timer
- 新系统/新项目:优先选 Systemd Timer
监控与排错¶
查看 Cron 是否在运行¶
查看 Cron 执行日志¶
调试不执行的定时任务¶
# 1. 看 Cron 服务是否运行
systemctl status cron
# 2. 看 Cron 日志有没有「执行了」
grep CRON /var/log/syslog | grep "your_script"
# 3. 模拟 Cron 环境运行脚本
env -i HOME=$HOME LOGNAME=$LOGNAME PATH=/usr/bin:/bin SHELL=/bin/sh sh -c '/opt/scripts/backup.sh'
# 4. 检查脚本权限
ls -la /opt/scripts/backup.sh
# 必须有执行权限!
# 5. 检查脚本末尾
# 脚本最后一行是否有换行符?
tail -c1 /opt/scripts/backup.sh | xxd
# 应该是 0a(换行符),不是空
生产环境最佳实践¶
- 脚本独立:不要在 crontab 里写复杂命令,写成独立脚本,crontab 只调用脚本
- 日志重定向:每条任务都加上
>> /var/log/xxx.log 2>&1 - 锁机制:防止上次任务没跑完,下次又启动了:
#!/bin/bash
LOCKFILE=/tmp/backup.lock
exec 200>"$LOCKFILE"
flock -n 200 || { echo "Another instance is running"; exit 1; }
# ... 实际的备份逻辑 ...
flock -u 200
- 避开整点:加上随机延迟,避免与大量其他任务同时触发(Cron 用
sleep $((RANDOM%60)),Timer 用RandomizedDelaySec) - 监控告警:关键任务的失败应该触发告警,而不是安静地失败
下一步学习¶
| 方向 | 教程 |
|---|---|
| systemd | systemd 详解 — Timer 的底层平台 |
| Shell 脚本 | Shell 基础 — 写更好的定时任务脚本 |
| 日志管理 | 日志管理 — 定时任务日志轮转 |
| 进程管理 | 进程管理 — 理解 Cron 守护进程 |
总结¶
定时任务是自动化运维的第一步:
- Cron 简单但坑多:环境变量、
%转义、输出丢失、夏令时——都记住了再上线 - Systemd Timer 更现代:有日志、有补执行、有随机延迟、有资源限制
- Anacron:非 7×24 机器的救星,错过也能补跑
- 生产环境:脚本独立 + 日志重定向 + 文件锁 + 监控告警
选对工具,写好脚本,加上监控——你的定时任务就不会是「设了但不知道跑没跑」的黑盒。 🚀