登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  linux

systemd timer 替代 cron 的日历表达式配置

来源:17golang原创

时间:2026-10-04 01:14:07 364浏览 收藏

把 cron 任务迁移到 systemd timer,关键不是把五段 cron 表达式机械翻译成另一串字符,而是先把“执行什么”和“何时执行”拆开:.service 负责命令、账户、工作目录与失败状态,.timer 负责日历表达式、补跑、精度和错峰。这样任务既能被单独启动测试,也能通过 systemd 的状态与日志入口排查。

本文用一个每天 02:30 执行的备份脚本做最小示例。假设脚本位于 /usr/local/sbin/backup-app,由 backup 用户运行。正式配置前,请确认脚本无需交互输入,并且所有路径都使用绝对路径。

官方地址:https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html
日历语法:https://www.freedesktop.org/software/systemd/man/latest/systemd.time.html

最小配方
  • 同名的 backup.timer 默认触发 backup.service,通常不必额外写 Unit=。
  • OnCalendar=*-*-* 02:30:00 表示每天本地时间 02:30。
  • Persistent=true 只对 OnCalendar 生效,用于补触发 timer 停用期间错过的日历任务。
  • 先用 systemd-analyze calendar 校验,再启用 timer。

先理解为什么要拆成两个 unit

cron 的一行同时装着时间和命令;systemd 则把两者分成独立 unit。backup.service 可以被手动执行,用来检查权限、环境与返回码;backup.timer 只负责在日历条件满足时激活它。官方手册说明,如果 timer 未显式配置 Unit=,它会默认激活与自己同名、仅后缀不同的 service。

例如 backup.timer 自动对应 backup.service。如果 timer 叫 nightly-backup.timer,service 也应叫 nightly-backup.service;只有确实要触发不同名称的 unit 时,才在 [Timer] 中写 Unit=other.service。

写出最小可用的 service 与 timer

先创建 /etc/systemd/system/backup.service。Type=oneshot 适合运行后退出的批处理任务。不要依赖交互式 shell 的 PATH、别名或当前目录;若脚本需要固定工作目录,应明确写 WorkingDirectory=。

# /etc/systemd/system/backup.service
[Unit]
Description=Backup application data

[Service]
Type=oneshot
User=backup
Group=backup
WorkingDirectory=/srv/app
ExecStart=/usr/local/sbin/backup-app

再创建 /etc/systemd/system/backup.timer:

# /etc/systemd/system/backup.timer
[Unit]
Description=Run application backup every day at 02:30

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
AccuracySec=1min

[Install]
WantedBy=timers.target

OnCalendar 使用实时时钟和日历语法。未指定时区时,它按系统当前时区解释;需要固定时区时可以把 IANA 时区名写在表达式末尾,例如 *-*-* 02:30:00 Asia/Shanghai。不要在同一组服务器上有的机器依赖本地时区、有的机器写固定时区,否则同一 unit 的实际触发时刻会难以比较。

backup.timer、OnCalendar、Persistent、AccuracySec、backup.service、ExecStart 与 timers.target 的静态关系图
图1:systemd timer 日历任务单元关系;各连线表示配置依赖,不表示实际执行顺序。

先用 systemd-analyze calendar 校验表达式

systemd-analyze calendar 会解析并标准化日历表达式,同时计算下一次触发时间。它比仅凭肉眼判断更可靠,尤其适合包含星期范围、列表和步进值的表达式。

# 校验每天 02:30 的表达式,并查看标准化结果与下一次触发。
systemd-analyze calendar '*-*-* 02:30:00'

# 校验工作日 09:00。
systemd-analyze calendar 'Mon..Fri *-*-* 09:00:00'

# 校验每小时从第 2 分钟开始、每 15 分钟一次的步进表达式。
systemd-analyze calendar '*:02/15'

第三个表达式会被补全为完整日期和秒字段。若真正想要每小时的第 0、15、30、45 分钟,可写 *:00/15 并以本机解析结果为准。systemd 还提供 daily、weekly、monthly 等简写,但在团队配置中写出完整时间通常更容易审核。

需求OnCalendar 示例说明
每天 02:30*-*-* 02:30:00日期字段全匹配
周一至周五 09:00Mon..Fri *-*-* 09:00:00星期名称使用英文
每月 1 日 04:10*-*-01 04:10:00日期固定为 01
每小时每 15 分钟*:00/15先用 calendar 子命令确认标准化结果
每天上海时间 02:30*-*-* 02:30:00 Asia/Shanghai不随主机默认时区变化

Persistent、AccuracySec 与随机延迟不要混用概念

Persistent=true 会把 service 上一次由 timer 触发的时间保存到磁盘。timer 再次激活时,如果停用期间至少错过了一次计划,就立即补触发一次;它不会把错过的每个时间点逐个重放。该选项只对配置了 OnCalendar 的 timer 有效,默认值是 false。

AccuracySec 是触发精度窗口,默认值为 1 分钟。它允许 systemd 在指定时间之后的一段窗口内合并唤醒,以减少无谓的 CPU 唤醒;若任务必须尽量靠近给定时刻,可以缩小该值,例如 AccuracySec=1s。没必要为普通夜间任务设置极小值。

RandomizedDelaySec 则主动在 0 到指定值之间增加随机延迟,用于让同一时间配置的大量任务错峰。它的默认值是 0。比如多台机器都在凌晨上传备份,可以加入:

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
AccuracySec=1s
RandomizedDelaySec=20min

这里先增加随机延迟,再受 AccuracySec 的合并窗口影响。前者是削峰,后者是允许合并唤醒,两者目标不同。若配置了随机延迟,Persistent=true 的补触发也仍会受该延迟约束。

加载、启用并查看下一次计划

配置写完后先让 manager 重新读取 unit,再启用 timer。通常只启用 .timer,不需要 enable 同名 oneshot service。

# 重新读取新增或修改的 unit 文件。
sudo systemctl daemon-reload

# 立即启动 timer,并让它在后续开机时进入 timers.target。
sudo systemctl enable --now backup.timer

# 查看该 timer 的最近触发与下一次计划。
systemctl list-timers --all backup.timer

# 查看 timer 当前状态及最后一次调度信息。
systemctl status backup.timer

list-timers 中的 NEXT 是下一次触发时间,LAST 是最近一次触发时间;它们确认的是“计划是否存在”。任务本身是否成功,还要看 service 状态和日志。

OnCalendar、systemd-analyze calendar、systemctl list-timers、Next、Last 与 journalctl 的静态对应关系图
图2:表达式、计划与日志的核对入口;定义是否正确和任务是否成功需要分别确认。

把执行结果和调度结果分开排查

在等待真实触发时间之前,可以先手动启动 service。这不会修改 timer 的日历表达式,适合确认脚本权限、路径和退出码。

# 单独测试执行单元;失败时命令返回非零状态。
sudo systemctl start backup.service

# 查看最近一次 service 状态。
systemctl status backup.service

# 查看本次开机以来该任务的日志。
journalctl -u backup.service -b --no-pager

# 查看 timer 自身的调度日志,不等同于脚本输出。
journalctl -u backup.timer -b --no-pager

若 backup.timer 显示 active 且有 NEXT,但 service 从未运行,先确认系统时间、时区与表达式;带 OnCalendar 的 timer 会自动排在 time-sync.target 之后,但缺少可靠实时时钟的设备仍应确保时钟同步服务真正完成校时。若 service 已被触发却失败,则回到 service 日志检查用户权限、工作目录、环境变量和脚本返回码。

从 cron 迁移时最容易踩的边界

现象原因处理
timer 激活但任务找不到命令脚本依赖交互式 shell 的 PATH在脚本和 ExecStart 中使用绝对路径,显式配置 Environment
机器开机后立刻执行Persistent=true 发现停机期间错过计划这是补跑语义;不需要补跑时设为 false
不是严格在整点执行AccuracySec 默认允许 1 分钟窗口按业务需要缩小,而非一律设成 1us
多台机器同时压垮后端所有 timer 使用相同计划且无错峰使用 RandomizedDelaySec,必要时再研究稳定随机偏移
修改 unit 后没有变化未执行 daemon-reload重新加载后 restart timer
只看 timer 误判任务成功timer 只负责激活 service同时查看 backup.service 状态与 journal

修改计划后的完整收尾

编辑已有 timer 后,不要重复 enable;重新加载并重启 timer 即可。再次运行 calendar 校验和 list-timers,可以同时覆盖“表达式能否解析”和“manager 是否已经采用新计划”两个问题。

# 修改 backup.timer 后重新加载并重启调度单元。
sudo systemctl daemon-reload
sudo systemctl restart backup.timer

# 重新检查 unit 内容和下一次计划。
systemctl cat backup.timer
systemctl list-timers --all backup.timer

如果只是每天、每周、每月这类日历计划,OnCalendar 加同名 service 已能替代大多数 cron 用法;若需求是“开机后延迟多久”“上次任务完成后再等多久”,应改用 OnBootSec、OnUnitActiveSec 或 OnUnitInactiveSec 等单调计时器,而不是继续堆叠日历表达式。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>