从 crontab 迁到 systemd timer:五个坑和一份迁移清单
用 crontab 跑了几年定时任务,最近把一台机器上的十几个作业整体迁到了 systemd timer。迁移过程比想象中顺利,但踩的坑也不少。这篇记录一下为什么换、换的时候最容易错在哪,给准备迁移的同学一份对照清单。
一、为什么想换
crontab 的问题不是不能用,而是出了问题不好查:
- 日志默认进系统邮件或 syslog,容器里经常啥都没有;
- 没有依赖关系,A 任务跑完再跑 B 只能靠
sleep硬等; - 机器关机错过的任务不会补跑(除非用 anacron);
- 语法难记,
*/5和0/5的区别每次都要查。
systemd timer 把这四件事都解决了:日志统一进 journald、可以依赖 service、支持错过补跑、语法有专门工具校验。
二、五个容易踩的坑
坑 1:PATH 和 cron 一样"干净",但不是同一份
很多人以为换成 systemd 就能读到 ~/.bashrc 里的环境变量,其实不能。systemd 的 user 实例和 system timer 都不会加载 shell 的 profile。表现就是:手动跑脚本一切正常,定时跑就报 command not found。
正确做法是显式声明,别指望继承:
[Service]
Type=oneshot
User=app
Environment=PATH=/usr/local/bin:/usr/bin:/bin
Environment=NODE_ENV=production
ExecStart=/usr/bin/node /opt/app/job.js
ExecStart 也建议写绝对路径。这一条能省掉九成的"手动能跑、定时不行"。
坑 2:OnCalendar 的写法和 cron 完全不是一回事
OnCalendar=*-*-* 06:00:00 表示每天 6 点。注意它不认 cron 的 */5,每 5 分钟要写 *:0/5。写完一定要验证,别靠猜:
systemd-analyze calendar '*-*-* 06:00:00'
# 会打印 Next elapse 和 UTC 时间,一眼就能确认对不对
systemd-analyze calendar '*:0/5' --iterations=3
时区也值得注意:OnCalendar 用的是系统时区(/etc/localtime)。如果服务器是 UTC,写 06:00 实际是北京时间 14:00。要固定北京时间,先把机器时区设成 Asia/Shanghai,再用 timedatectl 确认。
坑 3:错过的时间点不会补跑
这是从 cron 迁过来最容易忽略的一条。机器 3 点到 8 点关机,6 点的任务开机后默认不会执行。加上这一行才会补:
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
Persistent=true 会在开机后立刻补跑一次错过的任务(只补最后一次,不会把错过的十次都跑一遍)。
另外默认精度是 1 分钟,AccuracySec=1min。如果一堆任务都压在整点,可以把 AccuracySec=5min 放宽,或者加 RandomizedDelaySec=30s 错峰,避免同一秒把 CPU 和磁盘打满。
坑 4:日志不在 /var/log 里了
迁移后第一件事往往是"日志去哪了"。答案在 journald:
journalctl -u backup-job.service --since today
journalctl -u backup-job.service -f # 实时跟踪
journalctl --user -u myjob.service # 用户级 timer
想同时落文件,在 service 里加 StandardOutput=journal 之外,可以让脚本自己重定向,或者用 StandardOutput=append:/var/log/backup.log(systemd 240+)。
坑 5:oneshot 里不要让脚本自己后台化
Type=oneshot 的语义是"脚本跑完、退出码 0,这次执行就算成功"。如果脚本里 nohup xxx & 把真正的工作丢到后台再立刻退出,systemd 会认为任务已完成,而实际上失败了你也不知道,还会连带影响 Persistent 的补跑判断。
定时任务就老老实实前台跑完。真的需要常驻,那它就不是 timer 而是普通的 Type=simple 服务,应该用 Restart=always 管理。
三、一份可以直接抄的最小可用配置
/etc/systemd/system/backup-job.service:
[Unit]
Description=Daily backup
After=network-online.target
[Service]
Type=oneshot
User=app
Environment=PATH=/usr/local/bin:/usr/bin:/bin
ExecStart=/opt/scripts/backup.sh
TimeoutStartSec=30min
/etc/systemd/system/backup-job.timer:
[Unit]
Description=Run daily backup at 06:00
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
RandomizedDelaySec=30s
Unit=backup-job.service
[Install]
WantedBy=timers.target
启用和核对:
sudo systemctl daemon-reload
sudo systemctl enable --now backup-job.timer
systemctl list-timers --all | grep backup
四、迁移对照清单
| cron | systemd timer |
|---|---|
*/5 * * * * |
OnCalendar=*:0/5 |
0 6 * * * |
OnCalendar=*-*-* 06:00:00 |
@reboot |
不需要,用 WantedBy=timers.target + Persistent |
| 输出进 syslog | journalctl -u xxx.service |
| 无补跑 | Persistent=true |
小结
systemd timer 的学习成本主要在前半小时:搞清楚 .timer 和 .service 是一对、OnCalendar 要验证、Persistent 要显式打开。这三点过了之后,日志、依赖、补跑这些能力是白送的,长期看比 crontab 省心。
如果只是临时跑一次,systemd-run --on-calendar 更轻,连 unit 文件都不用写。生产环境我还是建议落成文件,便于版本管理和 review。
- 点赞
- 收藏
- 关注作者
评论(0)