Writing

用 systemd timer 做一件可观察的小事

从一个无害的定时示例理解 timer 与 oneshot:怎样安排时间、处理漏跑,并从日志确认它确实执行过。

为《用 systemd timer 做一件可观察的小事》确定性生成的文章图形
图形生成说明
函数家族
segmented-process
简化公式
y(t)=mix(qᵢ,qᵢ₊₁,smoothstep(t))
短哈希
0d98b3ecf475
生成器版本
2

这是由文章身份与结构生成的装饰图形,不是数据可视化。

下载 SVG下载生成清单

不少主机上已经有 cron,于是需要定时做一件小事时,第一反应往往是往 crontab 里补一行。systemd 环境里还有另一种更容易回看的做法:把“什么时候触发”和“实际运行什么”拆成两个 unit。前者是 .timer,后者通常是一次运行后退出的 .service

先让一条日志按时出现

下面用一个小实验走通配置、触发、查日志的过程。示例每周一早晨写一次当前时间到 journal,只执行 date,不做清理或业务数据修改。需要在一台使用 systemd 的测试主机上创建以下两个文件;已有同名 unit 时应另取名字,不要覆盖:

# /etc/systemd/system/weekly-note.service
[Unit]
Description=Write a weekly maintenance note

[Service]
Type=oneshot
ExecStart=/usr/bin/date '+weekly note: %%F %%T %%Z'
# /etc/systemd/system/weekly-note.timer
[Unit]
Description=Run weekly-note every Monday morning

[Timer]
OnCalendar=Mon *-*-* 09:10:00
Persistent=true
Unit=weekly-note.service

[Install]
WantedBy=timers.target

Type=oneshot 会等待命令完成。此例没有 RemainAfterExit=yes,正常执行后会回到 inactive,不能因此认定失败。ExecStart= 中的 %% 用于把百分号交给 date;这里不会经过 shell 展开。先用 command -v date 确认程序路径。

.timer 默认关联同名 .service;写出 Unit= 是为了方便对照。OnCalendar= 按主机时区解释这里的时间,不保证精确到指定秒触发。不确定表达式时,先用 systemd-analyze calendar 'Mon *-*-* 09:10:00' 看解析及下次时间;跨时区主机尤其要核对这一项。

启用之后,分别检查 timer 和 service

把文件放好后,先让 systemd 重读配置,再启用 timer:

sudo systemctl daemon-reload
sudo systemctl enable --now weekly-note.timer
systemctl list-timers weekly-note.timer

最后一条最有用。它能同时显示下次和上次触发时间;只看到 enabled 并不能说明任务已经运行。想在不用等待下周一的情况下检查服务本身,可以手动执行一次:

sudo systemctl start weekly-note.service
systemctl status weekly-note.service
systemctl show weekly-note.service -p Result -p ExecMainStatus
journalctl -u weekly-note.service -n 20 --no-pager

日志中应能看到 date 写出的那一行,Result=successExecMainStatus=0 是此例正常退出的预期。读取系统 journal 可能需要 sudo 或相应组权限。status 因服务已经 inactive 返回非零时,结合退出结果判断,不要只看一条命令的退出码。

替换 ExecStart= 前,还要确认账户、工作目录、环境变量和文件权限。系统级 service 默认可能以 root 运行,登录 shell 里测试成功也不保证服务环境中成功;真正的维护脚本应明确所需身份,避免顺手继承过大的权限。

漏跑一次,不等于补齐所有历史任务

Persistent=true 容易被理解成“无论错过多少次都会补齐”。它实际解决的是另一件事:带 OnCalendar= 的 timer 在机器关机或 timer 未运行期间错过了一个触发点,下一次重新激活后可以补做一次。它不是任务队列,也不会把停机期间的每一轮都逐个补回来。对有副作用的操作,这个区别很重要:例如删除、同步、发通知之前,要先判断补跑一次是否仍然合适。

timer 到点时,若目标 service 已经处于 active 状态,不会为它再创建一个并行实例。不过两个不同的 service 仍可能同时碰同一个目录或数据库。共享资源需要锁或明确的流程协调,把两个 timer 错开几分钟并不能保证互斥。

试完以后怎样停下

不再需要这项实验时,先取消定时触发:

sudo systemctl disable --now weekly-note.timer

这不会终止已经运行的 service,也不会删除 unit 文件和 journal 记录。确认 service 已结束后,再按创建时记录的路径处理这两个示例文件;删除或修改 unit 后需要 daemon-reload。真正上业务任务前,至少确认下一次触发时间、上次退出结果,以及补跑是否合适。

参考资料: