你对 systemd 定时器的爱还不够

Lobsters Hottest 工具

摘要

本文提倡在 Linux 上使用 systemd 定时器替代传统的 cron 作业来执行定时任务,强调其更好的集成性、日志记录以及更清晰的语法。

<p><a href="https://lobste.rs/s/9vt4ng/you_don_t_love_systemd_timers_enough">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/06/01 18:34

# 你对 systemd 定时器的爱还不够深 来源:https://blog.tjll.net/you-dont-love-systemd-timers-enough/ ### 《(https://blog.tjll.net/this-is-your-sign-to-self-host/)你对 systemd 定时器的爱还不够深 - 2026 年 5 月 5 日 - 2,139 字 - 阅读时间约 9 分钟 我最喜欢的转喻技术术语是“cron 任务”:即使 `cron` 字面上可能不是那个按计划执行操作的守护进程,我们也把任何行为举止像 `cron` 的东西都叫作“cron 任务”。正如 [Patrick McKenzie](https://x.com/patio11/status/1990437415383683416?s=20) 喜欢指出的那样,cron 任务是计算机领域最实用的原语之一。它们几乎对每个人都有许多几乎显而易见的用途:每天做*这个*;每月做*那个*。 然而。你*可能不应该*再使用字面上的 `cron`(或它更现代的衍生品)来执行定时任务了!在 2026 年,已经有更现代的选择可用,而我个人最钟爱的就是低调的 systemd 定时器。我热爱 systemd 定时器。如果你还没爱上它们,或许我可以告诉你为什么你也应该爱上它们。 #### *我的* `cron` 过时了? systemd *定时器*是一种单元类型,用于按特定计划调度其他单元(通常是服务)。(systemd *服务*单元如何工作属于另外一篇文章,但你可以将定时器目标的 `.service` 看作一个脚本。)定时器实际上是对传统 `cron` 守护进程的功能性替代(尽管你也可以同时运行两者),并且定时器的日历设置提供了某些相似之处,有助于从传统的类 cron 表达式过渡。 此时,systemd 黑子们开始冒头,试图 torpedo 定时器,因为它们是 systemd 项目的一部分,并且因为它们取代了成熟(尽管笨拙)的技术。我不愿把时间花在争论 `cron` 上,因此简要说明一下为什么像 systemd 定时器这样受益于多年经验教训的新方案更好: - 模糊的 `$PATH` 设置使得 `cron` 脚本执行难以预测。 - `stdout` 和 `stderr` 输出往往会掉入黑洞(而且常常发送到主机的邮件系统,这通常*不是你*想要的结果)。 - 执行历史难以跟踪和查询。 - 你可能会因为熟记调度语法而感到很酷,但 `01,31 04,05 1-15 1,6 \*` 对人类而言并不易读或直观。 顺便一提,定时器解决了所有这些(以及更多)问题。 #### 定时器入门的最佳时机 我们可以用简单的步骤来介绍基础知识。首先你需要一个定时器执行的目标。在运行 systemd 的 Linux 主机上,将以下单元内容放置到 `/etc/systemd/system/roulette.service` 即可安装一个服务,该服务有 1/10 的几率让你“自由”(即,关闭你的电脑): Systemd - 用于高亮字符串的字体。 - 用于高亮关键字的字体。 - 用于高亮类型和类名的字体。 ``` [Unit] Description=1 in 10 chance to break your chains [Service] ExecStart=/usr/bin/env bash -c '[[ $(($RANDOM % 10)) == 0 ]] && systemctl poweroff || echo LIVE ANOTHER DAY' ``` 更新:[2026-05-05 周二] Twitter 上的好友 [HSVSphere](https://x.com/HSVSphere) [指出](https://x.com/HSVSphere/status/2051695985147973828?s=20),服务选项 `ExecCondition=` 提供了一种原生方式来处理*条件性*执行。这是一种更紧密集成的方式来表达“是否应该继续执行”,我同意这能在单元级别更清晰地表达意图(这里我使用了 NixOS 系统的绝对路径): Systemd - 用于高亮字符串的字体。 - 用于高亮关键字的字体。 - 用于高亮类型和类名的字体。 ``` [Unit] Description=1 in 10 chance to break your chains [Service] ExecCondition=/run/current-system/sw/bin/bash -c '[[ $(($RANDOM % 10)) == 0 ]]' ExecStart=/run/current-system/sw/bin/systemctl poweroff ``` 这与之前的 `bash` 条件效果相同,并且在日志中你会看到不同的措辞(在我看来)更清晰地表达了条件满足时的状态: ``` May 05 11:05:32 diesel systemd[3117]: Condition check resulted in 1 in 10 chance to break your chains being skipped. ``` 一般来说,利用 systemd 提供的选项比自行编写脚本是更好的体验。(另一个例子是使用 `OnFailure=` 来响应服务脚本失败的情况,或者使用 `Restart=` 来尝试在临时故障后恢复。) 将该*服务*与一个*定时器*关联起来,需要将一个同名文件(`roulette`)放置到 `/etc/systemd/system/roulette.timer`: Systemd - 用于高亮关键字的字体。 - 用于高亮类型和类名的字体。 ``` [Unit] Description=impending destruction [Timer] OnCalendar=10:00 [Install] WantedBy=timers.target ``` 我所说的*关联*是指,默认情况下,定时器的 `Unit=` 设置会选择一条匹配名称后缀为 `.service` 的服务单元。在本例中是 `roulette.service`。如果你想执行另一个不同单元名的服务,随时可以更改。 我想立即指出几点: - 按照常规服务单元语义,`ExecStart=` 的目标*默认不作为 shell 命令运行*。你应该将绝对路径目标视为脚本,或者在我们的例子中,视为一个期望接收脚本作为字符串参数的解释器。例如,`ExecStart=/usr/bin/echo Hello \| /usr/bin/awk` 直接就不能工作;上下文中的管道在这里没有意义。 - `ExecStart=` 参数*默认不继承任何环境变量*(除了某些系统管理器的默认值),因此默认情况下 `$PATH` 非常精简。执行 `/usr/bin/env` 是一个快捷方式,以确保 `systemctl` 等命令可用,但开箱即用时,你得到的是一张白纸。如果我们在 `ExecStart` 中使用了裸的 `/usr/bin/bash`,则 `$PATH` 中会有基本工具,但这里使用 `env` 是一种额外的保障。 你也可以直接运行服务,而无需定时器辅助: shell 请注意,如果没有可用的 `[Install]` 部分,*你不能 `enable` 这个服务*:我们的定时器是让服务以一致方式运行的标准途径。另外值得指出的是,`systemctl` 默认操作 `roulette.service`,无需显式后缀。 当应用于 `.timer` 单元时,`systemctl start` 子命令会让它“上钟”,但并不会真正执行 `Unit=` 目标: shell ``` systemctl start roulette.timer ``` 现在*定时器*已激活,但*服务*尚未激活。 根据当前时间点,`status` 会告诉你下次定时器何时决定你的命运: shell ``` systemctl status roulette.timer ``` 你会在 `status` 页面上看到关于定时器的大量信息,包括下次触发时间: ``` Trigger: Sat 2026-04-18 10:00:00 MDT; 35min left ``` 这就是最简单的定时器入门:创建目标,将目标服务文件与一个包含计划的定时器放在一起,然后启动定时器(而非目标)来开始计划执行。因为 `.timer` 在 `[Install]` 中定义了 `WantedBy=`,我们可以确保定时器在启动时也会运行,而不仅仅是当我们手动 `start` 时: shell ``` systemctl enable roulette.timer ``` 让我们超越基础知识。 #### 时间领主 关于定时器,可以说最重要的信息是如何表达计划,无论是重复的时间段(手册通常称为时间跨度)还是日历事件(或时间戳)。幸运的是,我认为关于这个主题的 `man` 页面(`systemd.time(7)`)非常出色,提供了大量示例。你应该将它作为编写定时器时的*第一*参考资料;它比……呃……随便写的博客文章要好(或更好)。 systemd 还附带了一个名为 `systemd-analyze` 的命令行工具,它能够直接以命令式的方式验证和解释时间表达式,帮助你理解它们。你甚至可以消除经典通配符 `cron` 表达式的歧义,`systemd-analyzer` 可以解析并解释给你听,包括预期的执行时间: shell ``` systemd-analyze calendar '*-*-* *:*:*' ``` ``` Normalized form: *-*-* *:*:* Next elapse: Sat 2026-04-18 16:44:26 MDT (in UTC): Sat 2026-04-18 22:44:26 UTC From now: 431ms left ``` 这篇博客文章不是逐字复制 `systemd.time(7)` 全部内容的地方,所以我鼓励你去阅读有用的手册(RTHM)。简而言之,你可以简单地定义周期性的挂钟时间*或者*(与老旧的 `cron` 不同)相对于某个先前事件的周期性时间段。 第一类时间表达式很容易想象。例如,全限定形式的 `daily` 表示: ``` *-*-* 00:00:00 │ │ │ │ │ ╰── 第 00 秒 │ │ │ │ ╰───── 第 00 分 │ │ │ ╰──────── 第 00 时 │ │ ╰────────── 每一天 │ ╰──────────── 每个月 ╰────────────── 每一年 ``` 你可以使用 `daily` 这样的简写术语,写出完整形式,或者使用 `systemd.time(7)` 中列出的任何其他支持的值,然后通过 `systemd-analyze` 验证你的假设。 *第二*类时间表达式适用于“相对于某个其他事件运行”。与“每天同一时间运行”的区别往往正是你*实际*想要的。例如,考虑一个清理临时目录的任务:如果 `cron` 表达式在启动后立即过期,那么 `/tmp` 可能根本没什么可清理的。但是,如果你编码为“计算机启动后一小时执行,然后此后每小时执行一次”,则该调度逻辑对于相关服务的实际工作是有意义的。 在定时器中很容易做到: Systemd - 用于高亮关键字的字体。 - 用于高亮类型和类名的字体。 ``` [Timer] OnBootSec=1h OnUnitActiveSec=1h ``` 即:“机器启动后一小时运行”(会执行一次),以及“我的 `Unit=` 运行后一小时运行”(这隐式地使定时器无限重复)。 像这样的周期性时间跨度,比“在每个小时的这一分钟运行”这样的表达式,更频繁地适合“偶尔执行一次”的用例。另一个好例子是,每年 12 月我用来轮询 [Advent of Code](https://adventofcode.com/) API 的定时器,用于我为一些朋友编写的 Slack 机器人。`*/15` 的 cron 表达式尊重了他们 API 请求的“每 15 分钟”策略,但由于这是用 cron 语言表达的最简单方式,我相信它与其他所有人一起轮询 API 时会产生尖峰流量!我关心的是,在做出代码修复后启动我的定时器,它会在*无论何时*15 分钟流逝后运行,这很可能减少了猛冲群问题。 日历型与时间跨度型单元可能是与传统 cron 任务的最大概念飞跃,但定时器还提供了更多功能。 ##### 鸟瞰倒计时 我最喜欢用来了解机器定时器状况的高级命令是 `list-timers` 子命令。以下是我的主机摘要: shell ``` NEXT LEFT LAST PASSED UNIT ACTIVATES Mon 2026-04-20 15:15:00 MDT 1min 40s Mon 2026-04-20 15:00:05 MDT 13min ago zfs-snapshot-frequent.timer zfs-snapshot-frequent.service Mon 2026-04-20 15:32:16 MDT 18min Mon 2026-04-20 14:22:15 MDT 51min ago fwupd-refresh.timer fwupd-refresh.service Mon 2026-04-20 16:00:00 MDT 46min Mon 2026-04-20 15:00:05 MDT 13min ago logrotate.timer logrotate.service Mon 2026-04-20 16:00:00 MDT 46min Mon 2026-04-20 15:00:05 MDT 13min ago zfs-snapshot-hourly.timer zfs-snapshot-hourly.service Tue 2026-04-21 00:00:00 MDT 8h Mon 2026-04-20 09:43:22 MDT 5h 29min ago zfs-snapshot-daily.timer zfs-snapshot-daily.service Tue 2026-04-21 07:31:28 MDT 16h Sun 2026-04-19 20:15:47 MDT 7h ago systemd-tmpfiles-clean.timer systemd-tmpfiles-clean.service Mon 2026-04-27 00:00:00 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago zfs-snapshot-weekly.timer zfs-snapshot-weekly.service Mon 2026-04-27 01:09:27 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago fstrim.timer fstrim.service Mon 2026-04-27 04:28:38 MDT 6 days Mon 2026-04-20 09:43:22 MDT 5h 29min ago zpool-trim.timer zpool-trim.service Fri 2026-05-01 00:00:00 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-snapshot-monthly.timer zfs-snapshot-monthly.service Fri 2026-05-01 03:17:17 MDT 1 week 3 days Wed 2026-04-01 10:07:51 MDT 1 week 1 day ago zfs-scrub.timer zfs-scrub.service 11 timers listed. Pass --all to see loaded but inactive timers, too. ``` 通过一个命令你就能全面了解任何按定时器计划执行的任务。非常有用。 `list-timers` 是我经常使用的 systemd 子命令家族中的一员。其他有用的还包括 `list-units` 和 `list-paths`(后者是 `systemctl` 中较新的补充)。 ##### 暂停后的苏醒 让一台处于暂停状态的系统唤醒以执行重要脚本,即使你不在现场执行物理操作(比如掀开笔记本电脑的盖子),听起来是一项艰巨的任务,直到你发现 `WakeSystem=`: ``` WakeSystem= Takes a boolean argument. If true, an elapsing timer will cause the system to resume from suspend, should it be suspended and if the system supports this. ... ``` 你可以想象这样的功能的用途。在支持在更新前下载软件包更新的发行版上(例如 Arch 或 NixOS),你可以在深夜预取更新包,以便早上在键盘前进行更新,还有很多其他想法也可以应用。手册页强调,如果你希望执行完 `.service` 后系统重新挂起,你需要手动重新发起挂起。 ##### 分散化 我在几段前提到了猛冲群问题,这是系统问题:“当一组进程同时醒来时会发生什么?”如果世界上每个 Debian 系统都在 `00:00:00` 硬编码执行 `apt update`,那么午夜对所有人来说都会是糟糕的尖峰时间。 两个定时器选项 `FixedRandomDelay=` 和 `RandomizedOffsetSec=` 可以帮忙: ``` FixedRandomDelay= Takes a boolean argument. When enabled, the randomized delay specified by RandomizedDelaySec= is chosen deterministically, and remains stable between all firings of the same timer, even if the manager is restarted. ... RandomizedOffsetSec= Offsets the timer by a stable, randomly-selected, and evenly distributed amount of time between 0 and the specified time value. ... ``` 我在实际系统中使用过这个来检查软件更新。它不仅能帮助解决猛冲群问题,还能将执行均匀分布,确保行为一致,并避免中断性活动(如重启可能协调分布式服务的守护进程)。 总的来说,定时器选项非常可配置,并且暴露了很大程度的粒度(再次强调,所有这些都在 `man` 页面中解释过了)。 ##### 坚持持久性 这个选项特别适用于那些不应因笔记本电脑挂起而跳过、但可能不需要 `WakeSystem=` 的定时任务: ``` Persistent= Takes a boolean argument. If true, the time when the service unit was last triggered is stored on disk. When the timer is activated, the service unit is triggered immediately if it would have been triggered at least once during the time when the timer was inactive. ... ``` 如果你安排一个系统向配置管理系统签到,但主机曾经历过停机,那么给 `.timer` 加上 `Persistent=` 可能意味着是立即收敛到正确状态,还是……(下文截断,需继续翻译,但用户消息在此结束,我们需根据已有内容完成翻译。注意用户消息中最后一句不完整,我们按原文结束处理。) (原文此处中断,但按照要求,我们只需翻译给出的内容。用户消息以“immediately”结束,但句子不完整。我们保持原样,不添加推测内容。)

相似文章

Systemd 动态用户 (2020)

Lobsters Hottest

本文介绍了 systemd 动态用户,该功能在运行时为 systemd 单元创建临时 Unix 用户,无需手动管理服务用户。文中包含配置示例和用例。

改进我的自托管 Actions Runner 设置

Lobsters Hottest

作者通过用 systemd-nspawn 容器替换 Docker 来改进其自托管的 Gitea Actions Runner 设置,以提高安全性,并详细说明了配置和权衡。

人生苦短,别用慢终端

Lobsters Hottest

本文详细介绍了通过避免框架、缓存补全以及懒加载工具来加速终端启动的实用技巧,实现了30毫秒的shell启动。

我的新家庭服务器软件选择

Lobsters Hottest

一篇个人博客文章,详细描述了作者为新家庭服务器选择操作系统和服务管理软件的经历,比较了Synology、TrueNAS、Debian以及使用Runit的Void Linux。