暗色模式

systemd timer 定时任务实战:从单元文件到自动执行与日志排查

技术教程
2026-08-06
17
0

为什么用 systemd timer:crontab 的三个痛点

上一篇文章 《Linux crontab 定时任务实战》 讲完了 crontab 的用法,但 crontab 有三个天生短板:

  1. 日志割裂:脚本输出要么丢弃,要么手动重定向到文件,排查一次任务为什么没跑要翻好几个地方;
  2. 错过不补:服务器关机/睡眠时错过的任务直接跳过,不补跑;
  3. 无依赖管理:任务要等网络就绪、等另一个服务起来?crontab 表达不了,只能靠脚本里 sleep 硬扛。

systemd timer 是 systemd 自带的定时任务方案,一个 .timer 单元 + 一个 .service 单元配对使用,直接继承 systemd 全家桶的能力:日志统一进 journald、错过的任务可以补跑、支持依赖排序、触发时间可随机化、还能套用资源限制与沙箱

对比项crontabsystemd timer
最小粒度1 分钟秒级(可以精确到秒)
日志手动重定向或丢弃自动进 journald,journalctl -u 查看
错过任务跳过Persistent=true 可补跑一次
依赖排序不支持After= / Wants= 与 systemd 生态互通
随机延迟(避免整点并发)不支持RandomizedDelaySec=
环境变量继承登录 shell 的 PATH 等不继承,需在单元文件里显式声明
开机自启@rebootWantedBy=timers.target + enable

本文所有命令都在 Ubuntu 24.04.4 + systemd 255 演示服务器上真实执行过,截图均为实际输出,照着敲即可。

核心概念:一对单元文件

systemd timer 由两个同名的单元文件组成,systemd 按 basename 自动配对:

  • xxx.service —— 定义「做什么」,即到点后要执行的命令。定时任务一般用 Type=oneshot(执行完即退出,不留驻);
  • xxx.timer —— 定义「什么时候做」,即触发时间。[Timer] 段里写 OnCalendar= 等触发条件,[Install] 段写 WantedBy=timers.target

系统级单元放在 /etc/systemd/system/,用户级放在 ~/.config/systemd/user/(桌面 Linux 上用户级 timer 更常见,不需要 root)。启动时要 enable 的是 .timer 而不是 .service——enable 了 timer,它到点会自动拉起对应的 service。

实战:定时记录磁盘使用率

以一个常见的运维需求为例:每 10 分钟记录一次根分区磁盘使用率,追加到日志文件。完整链路:脚本 → service → timer → 启用 → 验证 → 日志排查。

第一步:编写记录脚本

cat > /usr/local/bin/disk-log.sh <<'SCRIPT'
#!/bin/bash
# 记录根分区磁盘使用情况, 追加到 /var/log/disk-usage.log
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 根分区已用 $(df -h / | awk 'NR==2 {print $5}'), 剩余 $(df -h / | awk 'NR==2 {print $4}')" >> /var/log/disk-usage.log
SCRIPT
chmod +x /usr/local/bin/disk-log.sh

脚本本身很简单:date 取时间,df -h / 取根分区使用率,追加写入日志。注意脚本要加执行权限,并建议用绝对路径——后面会讲到,定时任务的环境不像交互 shell 那么「顺手」。

第二步:编写 service 单元

# /etc/systemd/system/disk-log.service
[Unit]
Description=记录根分区磁盘使用率

[Service]
Type=oneshot
ExecStart=/usr/local/bin/disk-log.sh

Type=oneshot 表示命令跑完即退出,这是定时任务的标配。ExecStart 只写一行命令,多个命令可以拆成多个 ExecStart 行(顺序执行),也可以在脚本里完成。

第三步:编写 timer 单元

# /etc/systemd/system/disk-log.timer
[Unit]
Description=每 10 分钟记录一次磁盘使用率

[Timer]
OnCalendar=*:0/10
Persistent=true

[Install]
WantedBy=timers.target

OnCalendar=*:0/10 的含义是「每小时的第 0、10、20、30、40、50 分触发」,即每 10 分钟一次。Persistent=true 让系统错过触发(比如关机)后,下次开机补跑一次,后面详解。

第四步:重载并启用

写完单元文件后,先 daemon-reload 让 systemd 认识新单元,再 enable --now 启用并立即启动:

systemctl daemon-reload
systemctl enable --now disk-log.timer

下图是这台演示服务器上三个文件的实际内容与启用结果(enable 后 is-enabled 返回 enabled,同时 /var/lib/systemd/timers/ 下生成了 stamp-disk-log.timer 时间戳文件,供 Persistent 补跑判断使用):

单元文件的实际内容与定时器启用状态

第五步:验证时间表达式

OnCalendar 的语法比 crontab 的五字段复杂,写错不会报错,而是永远不触发(或触发时机和你想的不一样)。所以 systemd 提供了专门的验证工具 systemd-analyze calendar:

systemd-analyze calendar '*:0/10'
systemd-analyze calendar 'Mon..Fri *-*-* 02:30:00'

它会输出规范化形式下次触发时间。比如实测:

  • *:0/10 规范化后是 *-*-* *:00/10:00,下次触发落在下一个整 10 分;
  • 简写 daily 等价于 *-*-* 00:00:00(每天 0 点),hourly 等价于 *-*-* *:00:00(每小时整点),weekly 等价于 Mon *-*-* 00:00:00(周一 0 点)——注意 weekly 不是「每 7 天」也不是「每周任意一天」,而是固定在周一,这是最容易踩的简写坑。

第六步:查询状态

systemctl list-timers 列出所有定时器及下次触发时间,核心信息都在这一个命令里:

systemctl list-timers --all

下图是启用约 10 分钟后的真实输出:NEXT 列显示下次触发 08:30:00 还剩 9 分钟,LAST 列显示上次已在 08:20:20 触发过,PASSED 列显示已过去 38 秒:

systemd-analyze calendar 验证时间表达式与 list-timers 状态查询

注意一个细节:计划触发时间是 08:20:00,实际执行是 08:20:20——晚了 20 秒。这是因为 timer 默认有 AccuracySec=1min 的容忍窗口,允许在计划时间前后 1 分钟内触发,方便 systemd 把多个唤醒合并省电。对日志类任务毫无影响,若要精确到秒级触发,可以设 AccuracySec=1s

第七步:手动触发与日志排查

等自然触发太慢,排查问题时要能手动触发一次。直接 start 对应的 service 即可,完全不影响 timer 的下次计划:

systemctl start disk-log.service    # 手动触发一次,相当于"立即执行一次"

执行结果去哪看?不像 crontab 需要自己重定向,脚本的 stdout/stderr 会被 systemd 自动收进 journald:

journalctl -u disk-log.service --no-pager -n 5
cat /var/log/disk-usage.log

下图是真实执行记录:journald 里能看到 08:20:20 的自然触发和 08:21:03 的手动触发(Starting → Deactivated → Finished 三步),日志文件相应新增了两行「根分区已用 25%,剩余 28G」,而 list-timers 里 NEXT 依然是 08:30,手动触发没有打乱计划:

手动触发一次后,journalctl 与磁盘日志文件中的执行记录

这就是 timer 相对 crontab 最舒服的地方:任务跑没跑、跑了几次、卡在哪,journalctl -u 全看得见,不用再像 crontab 时代那样在 /var/log 下猜文件名。

OnCalendar 语法速查

OnCalendar 的完整语法是 星期 年-月-日 时:分:秒 [时区],省略的字段有默认值(日期省略表示每天,时间省略表示 00:00:00)。常用写法:

需求OnCalendar 写法
每天 3 点*-*-* 03:00:00daily*:0/24
每小时整点hourly
每 15 分钟*:0/15
每 5 分钟(含 0 分)*:0/5
工作日 9 点Mon..Fri *-*-* 09:00:00
每周一 2 点Mon *-*-* 02:00:00
每月 1 号 3 点*-*-01 03:00:00
每季度第一天*-1,4,7,10-01 00:00:00
每天 6:00 和 18:00*-*-* 06,18:00:00(或写两行 OnCalendar)
指定时区(UTC)*-*-* 03:00:00 UTC

验证建议统一用 systemd-analyze calendar '<表达式>',每次写完都先验证再上线,能省掉大量「为什么没触发」的排查时间。

三个值得了解的选项

Persistent=true:错过补跑

默认情况下,系统在计划时间点处于关机/休眠状态,这次任务就静默跳过了。加上 Persistent=true 后,systemd 会在 /var/lib/systemd/timers/ 下记录上次触发时间戳(截图里能看到 stamp-disk-log.timer),开机时发现上次触发时间早于计划时间,就立即补跑一次。注意:只补最近一次,不会把关机期间的多次全部补上。适合「每天备份一次,关机了回来也要补」这类场景。

RandomizedDelaySec:错峰执行

很多服务器都在同一分钟跑同一类任务(apt 更新、备份、日志轮转),会产生「整点风暴」。RandomizedDelaySec=15min 会在计划时间后随机延迟 0~15 分钟,打散执行:

[Timer]
OnCalendar=daily
RandomizedDelaySec=15min

两个实测结论值得记住:

  1. 不写这个选项,默认是不随机化的——我们在裸 timer 上实测 systemctl show 返回 RandomizedDelayUSec=0;
  2. Ubuntu 系统自带的 timer 几乎都显式配置了随机化:比如 apt-daily-upgrade.timerRandomizedDelaySec=60m,fstrim.timer100min。所以如果你看到 apt 定时任务并没有在计划时间准点跑,别慌,是故意随机错峰的。

想完全禁用随机化,显式写 RandomizedDelaySec=0 即可(光删掉配置行不一定够,系统策略可能带默认值)。

AccuracySec:触发精度

前面说过,默认 AccuracySec=1min 允许前后 1 分钟内触发。桌面/笔记本上可以放宽到 10min 省电;需要准点执行的任务(比如和外部系统对时)收紧到 1s

常见坑清单

  1. 忘了 systemctl daemon-reload:新增或修改单元文件后必须重载,否则 systemd 用的还是旧配置,enable 会报错或行为不变;
  2. timer 里忘了 WantedBy=timers.target:没有这行,enable 无法建立开机自启,重启后 timer 就没了;
  3. enable 了 service 而不是 timer:enable .service 只会让它开机自启(对于 oneshot 是开机跑一次),定时逻辑在 .timer 里,必须 enable .timer;
  4. 脚本里用相对路径/依赖 PATH:定时任务的环境和交互 shell 不同(PATH 里往往没有 /usr/local/bin 等),ExecStart= 一律写绝对路径,需要环境变量用 Environment= 显式声明;
  5. 脚本输出找不到:stdout/stderr 进 journald,用 journalctl -u xxx.service 查,不需要自己重定向日志文件(重定向也行,就是多此一举)。

什么时候继续用 crontab

crontab 没有过时,只是适用场景收窄了:单行简单任务、需要移植到非 systemd 环境的脚本、团队习惯使然。但对于「服务器上的正经运维任务」——有日志、有依赖、要补跑、要错峰——systemd timer 是更省心的默认选择。另外调试期可以先用 systemd-run 快速起一个临时 timer,不用写任何文件:

systemd-run --on-calendar='*:0/7' --unit=demo-run-test /bin/true
systemctl list-timers --no-pager | grep demo-run-test   # 临时 timer 生效
systemctl stop demo-run-test.timer                      # 用完即删

参考资料

发表评论

暂无评论,快来抢沙发吧!