用 systemd timer 替代 cron:日历、补跑和抖动
很多机器上还留着一行 crontab。它能跑,但失败时往往只剩一封没人看的邮件,或者什么都没有。systemd timer 把「什么时候触发」和「真正执行什么」拆成两个单元,触发记录进 journal,错过的时间点还可以补跑。这篇只讲用户级 timer 的最小可用写法,不改系统服务,也不讨论定时表达式的全部语法。
先准备一个只做一件事的 service。假设每天整理一次家目录临时文件夹里超过七天的普通文件,把定义写到 ~/.config/systemd/user/tidy-tmp.service:
[Unit]
Description=Remove stale files under tmp
[Service]
Type=oneshot
ExecStart=/usr/bin/find %h/tmp -type f -mtime +7 -delete
Type=oneshot 表示进程结束单元就结束,timer 不会把它当成常驻服务反复拉起。%h 是 systemd 的家目录说明符,比在单元里写死 /home/某人 更不容易在换账号时踩坑。命令用绝对路径,避免用户环境和非登录 shell 的 PATH 不一致。
再写同目录下的 tidy-tmp.timer:
[Unit]
Description=Daily tidy of tmp
[Timer]
OnCalendar=*-*-* 03:20:00
Persistent=true
RandomizedDelaySec=15min
[Install]
WantedBy=timers.target
OnCalendar 是日历事件,不是「每隔多久」。上面这行表示每天 03:20。机器当时如果关机,Persistent=true 会在下次启动后尽快补跑一次,而不是把这一轮静默丢掉。RandomizedDelaySec 把实际启动时间在十五分钟内错开,避免同一台机器上的多个任务挤在整点。目的和在 cron 里写 sleep 随机数一样,但延迟写在单元里,systemctl cat 就能看到,不用再去读脚本。
启用只针对当前用户:
systemctl --user daemon-reload
systemctl --user enable --now tidy-tmp.timer
systemctl --user list-timers
list-timers 会列出 NEXT 和 LAST。刚启用时 LAST 是空的,只说明还没跑过,不要据此判断单元损坏。想先验证命令,可以 systemctl --user start tidy-tmp.service,再用 journalctl --user -u tidy-tmp.service -n 50 看退出码。timer 激活和服务成功是两件事:timer 到点只负责拉起服务,服务返回非零要到 service 自己的日志里查。
和 cron 相比,有三处容易误判。第一,用户级 timer 默认跟着用户会话,图形界面退出后可能停止。如果希望开机后、尚未登录时也跑,对该用户执行 loginctl enable-linger,而不是急着把单元改成系统级。第二,改完文件必须 daemon-reload,否则内存里仍是旧定义,enable 看起来成功,行为却没变。第三,日历写错时,用 systemd-analyze calendar '*-*-* 03:20:00' 可以直接展开接下来几次触发点,比把 crontab 留到明天碰运气更快。
不建议一次把全部 cron 迁完。先选一个可重入、失败了也没有副作用的任务,连续两天核对 list-timers 的 LAST 和 journal 里的退出码,再决定其余条目要不要换。timer 带来的是可观察性和补跑,不是把调度本身变复杂。
还有一个容易和 cron 混淆的地方。OnUnitActiveSec 是单调计时,从上一次服务被激活开始再等一段间隔,适合「跑完再隔一小时」。OnCalendar 对齐的是墙上时钟。清理临时文件更适合后者,因为你关心的是每天固定窗口,而不是上次结束后再等二十四小时。服务如果偶尔多跑十分钟,单调间隔会把触发点一天天往后挪。
timer 和 service 应放在同一目录,并使用相同的主文件名,systemd 才会把它们配成一对。主文件名不一致时,要在 timer 里用 Unit= 显式指向,否则 list-timers 看得到触发,journal 里却是找不到单元。核对时优先看 systemctl --user status tidy-tmp.timer,确认 Loaded、Active 和 Trigger 三行,而不是只看 enable 命令有没有报错。
- 点赞
- 收藏
- 关注作者
评论(0)