为什么用 systemd timer:crontab 的三个痛点
上一篇文章 《Linux crontab 定时任务实战》 讲完了 crontab 的用法,但 crontab 有三个天生短板:
- 日志割裂:脚本输出要么丢弃,要么手动重定向到文件,排查一次任务为什么没跑要翻好几个地方;
- 错过不补:服务器关机/睡眠时错过的任务直接跳过,不补跑;
- 无依赖管理:任务要等网络就绪、等另一个服务起来?crontab 表达不了,只能靠脚本里 sleep 硬扛。
systemd timer 是 systemd 自带的定时任务方案,一个 .timer 单元 + 一个 .service 单元配对使用,直接继承 systemd 全家桶的能力:日志统一进 journald、错过的任务可以补跑、支持依赖排序、触发时间可随机化、还能套用资源限制与沙箱。
| 对比项 | crontab | systemd timer |
|---|---|---|
| 最小粒度 | 1 分钟 | 秒级(可以精确到秒) |
| 日志 | 手动重定向或丢弃 | 自动进 journald,journalctl -u 查看 |
| 错过任务 | 跳过 | Persistent=true 可补跑一次 |
| 依赖排序 | 不支持 | After= / Wants= 与 systemd 生态互通 |
| 随机延迟(避免整点并发) | 不支持 | RandomizedDelaySec= |
| 环境变量 | 继承登录 shell 的 PATH 等 | 不继承,需在单元文件里显式声明 |
| 开机自启 | @reboot | WantedBy=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.shType=oneshot 表示命令跑完即退出,这是定时任务的标配。ExecStart 只写一行命令,多个命令可以拆成多个 ExecStart 行(顺序执行),也可以在脚本里完成。
第三步:编写 timer 单元
# /etc/systemd/system/disk-log.timer
[Unit]
Description=每 10 分钟记录一次磁盘使用率
[Timer]
OnCalendar=*:0/10
Persistent=true
[Install]
WantedBy=timers.targetOnCalendar=*: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 秒:

注意一个细节:计划触发时间是 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,手动触发没有打乱计划:

这就是 timer 相对 crontab 最舒服的地方:任务跑没跑、跑了几次、卡在哪,journalctl -u 全看得见,不用再像 crontab 时代那样在 /var/log 下猜文件名。
OnCalendar 语法速查
OnCalendar 的完整语法是 星期 年-月-日 时:分:秒 [时区],省略的字段有默认值(日期省略表示每天,时间省略表示 00:00:00)。常用写法:
| 需求 | OnCalendar 写法 |
|---|---|
| 每天 3 点 | *-*-* 03:00:00 或 daily 或 *: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两个实测结论值得记住:
- 不写这个选项,默认是不随机化的——我们在裸 timer 上实测
systemctl show返回RandomizedDelayUSec=0; - Ubuntu 系统自带的 timer 几乎都显式配置了随机化:比如
apt-daily-upgrade.timer是RandomizedDelaySec=60m,fstrim.timer是100min。所以如果你看到 apt 定时任务并没有在计划时间准点跑,别慌,是故意随机错峰的。
想完全禁用随机化,显式写 RandomizedDelaySec=0 即可(光删掉配置行不一定够,系统策略可能带默认值)。
AccuracySec:触发精度
前面说过,默认 AccuracySec=1min 允许前后 1 分钟内触发。桌面/笔记本上可以放宽到 10min 省电;需要准点执行的任务(比如和外部系统对时)收紧到 1s。
常见坑清单
- 忘了
systemctl daemon-reload:新增或修改单元文件后必须重载,否则 systemd 用的还是旧配置,enable会报错或行为不变; - timer 里忘了
WantedBy=timers.target:没有这行,enable无法建立开机自启,重启后 timer 就没了; - enable 了 service 而不是 timer:enable
.service只会让它开机自启(对于 oneshot 是开机跑一次),定时逻辑在.timer里,必须 enable.timer; - 脚本里用相对路径/依赖 PATH:定时任务的环境和交互 shell 不同(PATH 里往往没有 /usr/local/bin 等),
ExecStart=一律写绝对路径,需要环境变量用Environment=显式声明; - 脚本输出找不到: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 # 用完即删
评论 (0)
暂无评论,快来抢沙发吧!