systemd Timers 使用说明
本文根据 Tyler Langlois 的文章 You Don't Love systemd Timers Enough 整理为中文版本。这里不做全文逐字翻译,而是保留核心观点、使用场景和关键命令,并用安全示例说明 timer 的写法。
cron job 已经变成了一个泛称:只要是定时执行任务,我们都会下意识叫它 cron。它足够简单,也足够古老,很多服务器维护脚本、日志清理、备份和巡检任务都从它开始。
但在一台已经运行 systemd 的 Linux 主机上,很多定时任务更适合交给 systemd timer。timer 不只是 cron 的替代品,它还继承了 systemd 的日志、状态、依赖、失败处理和用户级服务能力。
cron 的常见问题
传统 cron 能用,但经常让排障变得不清晰:
- 执行环境不直观,尤其是
$PATH和环境变量。 stdout和stderr经常被发送到本机邮件系统,或者被管理员忽略。- 想追踪某次任务是否运行、何时运行、失败原因是什么,通常不够顺手。
- 复杂时间表达式可读性差,例如
01,31 04,05 1-15 1,6 *这种写法很难一眼看出意图。
systemd timers 正好补上这些短板。它把定时任务拆成两个单元:
.service:真正要执行的动作。.timer:何时触发这个动作。
一个最小 timer 示例
下面用一个安全的示例代替原文中的玩笑式关机任务。目标是每天 10:00 在 journal 中写一条日志。
先创建服务文件 /etc/systemd/system/timer-heartbeat.service:
[Unit]
Description=Write a heartbeat line from a systemd timer
[Service]
Type=oneshot
ExecStart=/usr/bin/bash -lc 'echo "timer fired at $(/usr/bin/date --iso-8601=seconds)"'
再创建同名 timer 文件 /etc/systemd/system/timer-heartbeat.timer:
[Unit]
Description=Run timer-heartbeat.service every morning
[Timer]
OnCalendar=*-*-* 10:00:00
Persistent=true
[Install]
WantedBy=timers.target
两个文件的主干名都是 timer-heartbeat。默认情况下,timer-heartbeat.timer 会触发同名的 timer-heartbeat.service。如果想触发不同名称的服务,可以在 [Timer] 中显式写 Unit=。
启用并立即启动 timer:
sudo systemctl daemon-reload
sudo systemctl enable --now timer-heartbeat.timer
查看下一次触发时间:
systemctl status timer-heartbeat.timer
systemctl list-timers timer-heartbeat.timer
查看服务执行日志:
journalctl -u timer-heartbeat.service
如果只是想手动试跑服务,不需要等 timer:
sudo systemctl start timer-heartbeat.service
注意:日常应该启动和启用 .timer,不是 .service。服务负责执行一次动作,timer 才负责让它按计划重复运行。
ExecStart 不是 shell
这是从 cron 迁移到 systemd unit 时最容易踩的坑。ExecStart= 默认不是一整行 shell 命令,它更像是一个可执行文件加参数列表。
下面这种写法不会得到你期待的管道效果:
ExecStart=/usr/bin/echo hello | /usr/bin/tr a-z A-Z
如果确实需要 shell 特性,例如管道、重定向、变量展开或命令替换,需要显式调用 shell:
ExecStart=/usr/bin/bash -lc 'echo hello | /usr/bin/tr a-z A-Z'
更稳妥的做法是把复杂逻辑写进脚本,然后在 unit 中调用脚本的绝对路径:
ExecStart=/usr/local/sbin/run-daily-maintenance
这样 unit 文件负责调度和生命周期管理,脚本负责业务逻辑,排障边界也更清楚。
写时间表达式
timer 最常见的触发方式有两类。
第一类是日历时间,也就是类似 cron 的“在某个墙上时钟时间运行”:
[Timer]
OnCalendar=daily
daily 等价于每天零点。你也可以写得更具体:
[Timer]
OnCalendar=Mon..Fri 09:30
写 timer 前,建议直接用 systemd-analyze calendar 验证表达式:
systemd-analyze calendar 'Mon..Fri 09:30'
systemd-analyze calendar 'daily'
第二类是相对时间,也就是“在某个事件之后运行”。例如开机 10 分钟后运行一次,之后每隔 1 小时运行一次:
[Timer]
OnBootSec=10min
OnUnitActiveSec=1h
这种写法常常比固定整点更贴近任务意图。比如清理临时目录、刷新缓存、同步配置状态等任务,并不一定需要卡在每天某个固定分钟运行。
list-timers 给你全局视图
systemctl list-timers 是管理 timer 时最有用的命令之一:
systemctl list-timers
systemctl list-timers --all
它能看到每个 timer 的下一次触发时间、上一次触发时间、对应的 service,以及当前是否仍处于加载状态。相比在多个 crontab 文件里找任务,这个视图更适合日常巡检。
常用排障命令可以放在一起记:
systemctl status timer-heartbeat.timer
systemctl status timer-heartbeat.service
journalctl -u timer-heartbeat.service
systemctl cat timer-heartbeat.timer
systemctl cat timer-heartbeat.service
避免所有机器同时运行
如果大量机器都在同一时刻执行更新、拉取配置或访问外部 API,容易形成瞬时流量尖峰。timer 提供了随机延迟选项来打散执行时间:
[Timer]
OnCalendar=daily
RandomizedDelaySec=30min
FixedRandomDelay=true
这表示 timer 仍然按每天运行的策略触发,但实际执行会在 30 分钟窗口内分散。FixedRandomDelay=true 会让同一 timer 的随机偏移保持稳定,避免每次启动后都重新漂移。
如果你的 systemd 版本支持,也可以用稳定随机偏移表达更宽的分布窗口:
[Timer]
OnCalendar=*-*-* 03:00:00
RandomizedOffsetSec=6h
这类选项适合更新检查、远程上报、批处理轮询和分布式维护任务。
不想错过的任务用 Persistent
笔记本睡眠、服务器停机维护、虚拟机暂停,都可能让一次计划任务错过触发时间。对“不应该跳过”的任务,加上:
[Timer]
Persistent=true
当 timer 下次被激活时,如果它发现本来应该在离线期间触发过,就会尽快补跑一次。系统更新检查、配置收敛、批处理拉取等任务都适合使用它。
需要唤醒机器时用 WakeSystem
某些任务可能需要在机器挂起时唤醒系统执行,例如夜间预取更新包:
[Timer]
OnCalendar=03:30
WakeSystem=true
这个能力依赖硬件和发行版支持。还要注意,WakeSystem=true 只负责唤醒并触发任务,不代表任务完成后系统会自动重新挂起。如果需要重新挂起,要在服务逻辑中显式处理。
用户级 timer
timer 不一定只能跑在系统级 manager 下。用户级 timer 适合桌面会话相关任务:
systemctl --user daemon-reload
systemctl --user enable --now my-task.timer
systemctl --user list-timers
用户级 unit 常见位置是:
~/.config/systemd/user/my-task.service
~/.config/systemd/user/my-task.timer
用户级 timer 的 [Install] 目标经常写成:
[Install]
WantedBy=default.target
如果希望用户未登录时仍能运行用户级服务,还需要理解并配置 linger:
loginctl enable-linger username
迁移建议
把 cron 任务迁移到 systemd timer 时,可以按这个顺序处理:
- 把实际动作抽成一个脚本,确保脚本可以手动运行。
- 写一个
Type=oneshot的.service,只负责调用脚本。 - 写同名
.timer,用OnCalendar=或OnBootSec=/OnUnitActiveSec=表达触发策略。 - 用
systemd-analyze calendar验证时间表达式。 - 用
systemctl enable --now name.timer启用 timer。 - 用
systemctl list-timers和journalctl -u name.service做运行检查。
小结
systemd timers 的价值不在于“语法比 cron 新”,而在于它把定时任务放进了同一套 Linux 服务管理模型里:
- 日志进入 journal,失败原因更容易追踪。
- 状态可以用
systemctl status查看。 - 时间表达式可以用
systemd-analyze calendar验证。 - 可以使用
Persistent=补跑错过的任务。 - 可以使用随机延迟减少同时触发。
- 可以配合
OnFailure=、Restart=、依赖关系和用户级服务。
如果只是每小时 echo 一行文本,cron 当然还能工作。但当任务开始涉及日志、失败处理、依赖关系、错过补跑或大规模机器分散执行时,systemd timer 通常是更清晰的选择。