定时任务:cron 还是 systemd timer
「脚本手动跑得好好的,放进 cron 就不执行」——这是定时任务最经典的坑。理解两者的差别,就少踩很多。
一、怎么选
| 场景 | 推荐 |
|---|---|
| 简单的周期性任务(备份、清理) | cron,够用 |
| 需要依赖环境变量、服务上下文 | systemd timer |
| 需要「错过就补跑」 | systemd timer(Persistent=true) |
| 需要精确到秒 | 都不行,用循环 + sleep |
| 已有 systemd 服务,想定时跑 | systemd timer |
| 容器里 | 一般都没有,用宿主机或 k8s CronJob |
我的默认:简单任务用 cron,重要任务用 systemd timer。
二、cron 语法
crontab -e # 编辑当前用户的任务
crontab -l # 列出
crontab -r # 删除全部(危险)
sudo crontab -u www-data -e # 指定用户# 分 时 日 月 周 命令
# │ │ │ │ │
# │ │ │ │ └─ 0-7(0 和 7 都是周日)
# │ │ │ └──── 1-12
# │ │ └─────── 1-31
# │ └────────── 0-23
# └───────────── 0-59
30 3 * * * /opt/scripts/backup.sh # 每天 3:30
0 */4 * * * /opt/scripts/sync.sh # 每 4 小时
15 2 * * 1 /opt/scripts/weekly.sh # 每周一 2:15
0 0 1 * * /opt/scripts/monthly.sh # 每月 1 号
@daily /opt/scripts/daily.sh
@reboot /opt/scripts/onboot.sh系统级任务放 /etc/cron.d/:
# /etc/cron.d/baize
# 比用户 crontab 多一个「用户」字段
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
30 3 * * * root /opt/scripts/backup.sh >> /var/log/backup.log 2>&1三、cron 的经典坑
1. 环境变量几乎为空
cron 的执行环境只有极简的 PATH(通常 /usr/bin:/bin)。你的脚本里用了 /usr/local/bin/node、python3.11,就找不到。
# 错误示范:手动能跑,cron 里失败
node /opt/app/run.js
# 解法一:写绝对路径
/usr/local/bin/node /opt/app/run.js
# 解法二:crontab 顶部声明 PATH
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# 解法三:脚本里自己 source
source /etc/profile2. 没有输出 = 看不到错误
cron 的输出默认会尝试发邮件(多数机器没配,直接丢弃)。一定要重定向到日志:
30 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# ^^ 追加 ^^ 把错误也收进来3. 百分号要转义
cron 里 % 有特殊含义(换行)。date +%F 会出错:
0 3 * * * cp f f-$(date +\%F) # 必须转义或者把 date 写进脚本里(更推荐)。
4. 末尾要有换行
crontab 文件最后一行没有换行符,该任务会被忽略。
5. 机器关机时任务不会补跑
这是 cron 与 systemd timer 最大的差别之一。
四、systemd timer
需要两个文件:.service + .timer。
# /etc/systemd/system/backup.service
[Unit]
Description=每日备份
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=root
Environment="PATH=/usr/local/bin:/usr/bin:/bin"# /etc/systemd/system/backup.timer
[Unit]
Description=每天 3:30 跑备份
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true # 关机错过的话,开机后补跑
RandomizedDelaySec=300 # 随机延迟,避免多台机器同时跑
AccuracySec=1min
[Install]
WantedBy=timers.targetsudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
# 查看
systemctl list-timers --all
systemctl status backup.timer
# 手动触发一次
sudo systemctl start backup.service
# 看日志(这是最大优势)
journalctl -u backup.service
journalctl -u backup.timerOnCalendar 表达式
OnCalendar=*-*-* 03:30:00 # 每天 3:30
OnCalendar=Mon *-*-* 02:00:00 # 每周一 2:00
OnCalendar=*-*-01 00:00:00 # 每月 1 号
OnCalendar=hourly # 每小时
OnCalendar=daily
OnCalendar=weekly
OnBootSec=10min # 开机后 10 分钟(相对时间)
OnUnitActiveSec=1h # 上次执行后 1 小时(适合做轮询)
# 验证表达式写得对不对
systemd-analyze calendar "*-*-* 03:30:00"
# 会显示下一次触发时间五、对比表
| 维度 | cron | systemd timer |
|---|---|---|
| 配置 | 一行 | 两个文件 |
| 日志 | 自己重定向 | journalctl 直接看 |
| 环境 | 极简,易踩坑 | 可指定 Environment |
| 补跑 | ❌ | ✅ Persistent=true |
| 随机延迟 | 要自己写 sleep | ✅ RandomizedDelaySec |
| 依赖管理 | ❌ | ✅ After=/Requires= |
| 资源限制 | ❌ | ✅ MemoryMax= 等 |
| 精确到秒 | ❌ | ✅ |
六、任务重叠问题
上次任务没跑完,下次又启动了。解法:
# 方案一:脚本里加锁(flock,简单有效)
30 3 * * * flock -n /tmp/backup.lock /opt/scripts/backup.sh
# -n 表示拿不到锁就立即退出,不会堆积
# 方案二:脚本开头自己判断
if pgrep -f "backup.sh" | grep -v $$ > /dev/null; then
echo "上一个还没跑完,退出"
exit 0
fisystemd 天然不会重叠(同一个 service 不会并发启动)。
七、监控:任务到底跑了没
这是最容易被忽略的。备份脚本悄悄失败了半年,真出事才发现——我见过太多次。
# 1. 脚本成功时写一个心跳文件
date > /var/run/last-backup
# 2. 另一个任务检查心跳是否新鲜
#!/usr/bin/env bash
STAMP=/var/run/last-backup
if [ ! -f "$STAMP" ]; then echo "备份从未成功过!"; exit 1; fi
AGE=$(( ($(date +%s) - $(stat -c %Y "$STAMP")) / 3600 ))
if [ "$AGE" -gt 26 ]; then echo "备份已 $AGE 小时没成功!"; exit 1; fi更简单的方式:用外部监控服务(如 healthchecks.io 的 ping URL),脚本结束前 curl 一下,没按时 ping 就告警。
八、检查清单
- 命令用绝对路径,或声明了 PATH
- 输出重定向到日志文件
- 加锁,防止重叠
- 脚本本身
set -euo pipefail - 有失败告警或心跳检查
date +%F里的%已转义(cron)- 用
systemd-analyze calendar验证过时间表达式
定时任务的关键不是「跑起来」,而是跑失败了你能知道。
