用 systemd timer 替代 cron:更可控的定时任务

1379 字
7 分钟
用 systemd timer 替代 cron:更可控的定时任务

定时任务几乎是每台服务器的标配:备份、清理、同步、拉取证书。多数人第一反应是 crontab -e,写一行就走人。cron 确实简单,但当任务变多、出错、需要排查时,它的短板会集中暴露——脚本执行环境和交互式 shell 不同、输出被邮件吞掉、任务重叠执行、机器关机期间的计划直接丢失。

systemd 已经是主流发行版的 init 系统,它自带的 timer 单元可以完整替代 cron,并且把定时任务纳入统一的服务管理与日志体系。本文记录一套可直接套用的写法。

与 cron 对比#

维度cronsystemd timer
配置形式单行表达式两个单元文件(.service + .timer
日志默认发邮件,常常无人查看统一进 journal,journalctl -u 直接看
执行环境极简 PATH,与登录 shell 不同显式声明,行为可预期
错过的计划关机期间直接丢失Persistent=true 开机后补跑
任务重叠上次没跑完也会再起一个同一 service 天然串行,不会重入
随机延迟需要自己 sleep $RANDOMRandomizedDelaySec= 一行搞定
资源限制可用 cgroup 限制 CPU / 内存
依赖关系可声明 After=Requires=
手动触发只能手工执行脚本systemctl start xxx.service
上次/下次执行需自行记录systemctl list-timers 一览

代价是配置变啰嗦了:一件事要写两个文件。换来的是可观测性和确定性,任务一多就很划算。

一个完整示例#

假设需要每天凌晨做一次备份脚本。先写 service 单元,描述「做什么」:

/etc/systemd/system/backup.service
[Unit]
Description=Daily backup job
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
User=backup
Group=backup
Environment="LANG=C.UTF-8"
WorkingDirectory=/srv/backup
ExecStart=/usr/local/bin/backup.sh
TimeoutStartSec=30min
# 简单加固
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
NoNewPrivileges=true
ReadWritePaths=/srv/backup

注意 Type=oneshot:跑完就退出,不是常驻服务。没有 [Install] 段是刻意的,这个 service 不需要开机自启,由 timer 拉起。

再写 timer 单元,描述「什么时候做」:

/etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily
[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
RandomizedDelaySec=15min
AccuracySec=1min
Unit=backup.service
[Install]
WantedBy=timers.target

两个文件同名(backup),systemd 会自动关联,Unit= 其实可省略,写出来更直白。

启用:

Terminal window
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
# 看下次触发时间
systemctl list-timers backup.timer
# 不等定时,立刻手动跑一次验证
sudo systemctl start backup.service
# 看这次跑的输出
journalctl -u backup.service -n 50 --no-pager

OnCalendar 的语法比 cron 表达式易读得多,写完可以用 systemd-analyze 校验:

Terminal window
systemd-analyze calendar "Mon..Fri 09:00"
systemd-analyze calendar "*-*-01 04:00:00"
systemd-analyze calendar "hourly"

它会打印规范化结果和下一次触发时间,比在脑子里推算 0 3 * * * 靠谱。

除了绝对时间,还有相对触发方式,适合「开机后 10 分钟跑一次,之后每 6 小时一次」这类需求:

[Timer]
OnBootSec=10min
OnUnitActiveSec=6h

常见坑#

1. 改了单元文件没有 daemon-reload systemd 读的是缓存后的配置,改完必须 systemctl daemon-reload,否则你会对着「明明改了却没生效」发呆。

2. enable 错了对象。enable 的是 .timer 而不是 .service。启用 service 意味着开机就跑一次,和你的意图无关。

3. 以为 service 里能用 shell 语法。 ExecStart= 不经过 shell,管道、重定向、通配符、&& 都不会被解析。需要这些就调用脚本文件,或者显式写成:

ExecStart=/bin/bash -c 'foo | bar > /var/log/out.log'

4. PATH 和环境变量与登录 shell 不同。 service 里的 PATH 很短,nodepnpmdocker 之类常因为版本管理器装在用户目录而找不到。稳妥做法是在 ExecStart 里写绝对路径,或用 Environment= 显式补全。

5. Persistent=true 的补跑行为要想清楚。 它会在开机后立即补执行错过的任务。对备份、证书续期是好事;对「每天推送一次通知」类任务,可能在开机瞬间一次性触发一堆,需要评估。

6. 相对时间的基准容易记混。 OnUnitActiveSec= 从上次启动时刻算起,OnUnitInactiveSec= 从上次结束时刻算起。任务耗时长时两者差别明显。

7. 忘了任务超时。 Type=oneshot 默认会等任务结束,卡住的进程可能一直挂着。用 TimeoutStartSec= 给个上限,超时自动杀掉,不至于挡住下一轮。

8. 用户级 timer 的存活问题。 放在 ~/.config/systemd/user/ 下的 timer 由 systemctl --user 管理,用户退出登录后默认会被清掉。需要常驻就执行一次 loginctl enable-linger <用户名>

9. 集群里所有机器同一秒触发。 多台机器同时拉取同一个上游,容易把对方打崩。RandomizedDelaySec= 把触发点打散,比在脚本里 sleep $((RANDOM % 300)) 干净得多。

排查手册#

几条日常够用的命令:

Terminal window
# 所有 timer 的上次/下次执行
systemctl list-timers --all
# 某个任务的历史日志(含退出码)
journalctl -u backup.service --since "3 days ago"
# 实时跟踪
journalctl -u backup.service -f
# 看单元最终生效的完整配置(含 drop-in 覆盖)
systemctl cat backup.service
systemd-analyze verify /etc/systemd/system/backup.service

systemctl status backup.service 会显示上次退出码,脚本里记得让失败路径 exit 非零,否则 systemd 认为一切正常。

小结#

cron 依然适合「一行搞定、跑挂了也无所谓」的临时任务。但只要任务需要被观测、需要处理错过与重叠、需要限制资源或声明依赖,systemd timer 的多写几行就非常值得。

迁移路径也不激进:不必一次性清空 crontab,可以先把最关键、最容易出问题的那几个任务改成 timer,跑上两周看看日志是否更好排查,再决定要不要继续推进。

—— 小鱼人

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

用 systemd timer 替代 cron:更可控的定时任务
https://blog.727221.xyz/posts/2026-07-26-linux-systemd-timer-for-recurring-tasks/
作者
小鱼人
发布于
2026-07-26
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
小鱼人
技术 / 运维 / 随笔。
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签
站点统计
文章
4
分类
3
标签
16
总字数
6,057
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.15.0
文章许可
CC BY-NC-SA 4.0