发布时间:2026-09-22 04:31 更新时间:2026-09-22 04:31 阅读量:0
用 crontab 跑定时任务,最让人头疼的不是语法,而是它太“沉默”:机器关机期间错过的任务不会补跑,任务失败没有通知,多个任务挤在同一分钟启动容易把磁盘 IO 打满,想查日志还得自己重定向到文件。systemd timer 本质上是把定时触发交给 systemd 统一调度,任务本体仍然是一个普通的 service,于是日志、依赖、重启策略、资源限制这些 systemd 已有的能力全都能复用。本文按“先能跑、再讲可靠性”的顺序,把 timer 与 service 的配合写法、OnCalendar 表达式、随机延迟和失败重试讲清楚。下文命令与配置在主流发行版上通用,个别字段以本机 man systemd.timer 和官方文档为准。
systemd 的设计是“一个 timer 触发一个同名 service”。约定俗成的做法是建两个文件:/etc/systemd/system/backup.service 负责干活,/etc/systemd/system/backup.timer 负责什么时候干。timer 里不需要写 ExecStart,只要用 Unit= 指向 service 即可;不写 Unit 时,systemd 会默认寻找同名的 .service。
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly data backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
失败后最多再试 2 次,间隔 5 分钟
Restart=on-failure
RestartSec=5min
StartLimitBurst=3
StartLimitIntervalSec=1800
这里有两个关键点。第一,Type=oneshot 表示任务执行完就退出,systemd 会把它当成一次性的,适合脚本类作业;如果脚本里有后台进程,用 oneshot 会导致 systemd 认为任务已结束而误判,这种情况建议改用 Type=simple 并在脚本里做好前台阻塞。第二,Restart=on-failure 是失败重试的核心,但要注意它和 StartLimitBurst 配合:如果 30 分钟内连续失败超过 3 次,systemd 会停止重试并进入 failed 状态,避免无限重启把机器拖垮。这样设计比 crontab 里手动写循环更可控。
对应的 timer 文件如下:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup every night
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15min
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target
写完执行 systemctl daemon-reload,再 systemctl enable --now backup.timer。注意启用的是 timer 而不是 service,service 由 timer 拉起。验证是否生效可以用 systemctl list-timers,它会列出下一次触发时间,这是排查定时任务最直观的入口。
OnCalendar 的格式是 星期 年-月-日 时:分:秒,其中日期部分可以用通配符,也可以用 .. 表示区间、, 表示枚举。它比 cron 的五段式更易读,支持“每 5 分钟”“每月 1 号”这类语义。常见的几种写法:
# 每天凌晨 3:30
OnCalendar=*-*-* 03:30:00
每 10 分钟一次
OnCalendar=*:0/10
每周一、周四 02:00
OnCalendar=Mon,Thu *-*-* 02:00:00
每月 1 号 04:15(注意日字段用 01 更清晰)
OnCalendar=*-*-01 04:15:00
如果拿不准表达式是否合法,可以用 systemd-analyze calendar 直接验证,它会打印下次触发时间以及归一化后的表达式,这是避免写错日期最省事的办法:
systemd-analyze calendar "Mon,Thu *-*-* 02:00:00"
systemd-analyze calendar "*:0/10" --iterations=3
再说 RandomizedDelaySec。当你有几十台机器都跑同一个 timer 时,整点同时触发会造成源站或数据库的瞬时压力。这个参数会在触发时间上叠加一个 0 到设定值之间的随机延迟,把负载摊开。它只影响 timer,不影响 service 的执行逻辑。需要注意的是,随机延迟会让“实际开始时间”变得不确定,如果业务对时间窗口有硬要求(比如必须在 4 点前跑完),随机值就不要设得太大,具体上限结合任务耗时评估。
crontab 最被诟病的一点是机器关机期间错过的任务永远不会补。timer 的 Persistent=true 解决了这个问题:systemd 会记录该 timer 上次触发的时间戳(存放在 /var/lib/systemd/timers/ 下),开机后如果发现已经错过了本该执行的时间点,会立即补跑一次。对于备份、日志归档、对账这类不能漏的作业,这个开关基本必开。
不过 Persistent 补跑的是“最后一次错过”,如果停机了三天,它不会把三天的任务各补一次,而是只补一次。这一点要在脚本里自行处理增量逻辑。另外补跑发生的时间点是系统启动之后,如果任务本身依赖网络或挂载,记得在 service 里用 After=network-online.target 声明依赖,否则可能因为网络未就绪而失败。
失败之后怎么知道?两种做法。一种是利用 Restart=on-failure 做自动重试,适合瞬时故障;另一种是配置 OnFailure= 指向一个通知用的 service,在任务最终失败时执行告警脚本,比如发邮件或调用 Webhook。示例:
# /etc/systemd/system/backup.service 中追加
[Unit]
OnFailure=notify@%n.service
/etc/systemd/system/notify@.service
[Unit]
Description=Send failure notice
[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify.sh "%i failed"
调试阶段建议先手动触发一次 service,确认脚本能跑通,再看 timer 是否按时拉取:systemctl start backup.service 看退出码,journalctl -u backup.service -n 50 看日志输出。如果 timer 没触发,优先检查 systemctl list-timers --all 里状态是否为 active,以及是否被 systemctl mask 屏蔽。整体迁移时,建议一个任务一个 service 加一个 timer,命名保持对应,方便后续维护和批量排查,具体细节以本机 systemd 版本的官方文档为准。
| 📑 | 📅 |
|---|---|
| Docker容器时间漂移与crond定时任务错乱:TZ与宿主时间源协同 | 2026-09-21 |
| MySQL 授权与远程访问配置实战:user@host 匹配规则、bind-address 与防火墙三层放行检查 | 2026-09-21 |
| Nginx与PHP上传目录权限:www-data、umask与0777的坑 | 2026-09-21 |
| systemd-resolved 与 /etc/resolv.conf 冲突排查 | 2026-09-21 |
| MySQL索引失效排查:EXPLAIN执行计划与隐式转换定位 | 2026-09-21 |
| Nginx 代理 WebSocket 频繁断连:Upgrade 头、超时与保活配置 | 2026-09-22 |
| 服务器被DDoS打满带宽:自查连接分布与限速缓解 | 2026-09-22 |
| Linux磁盘IO高却查不出:iotop与blkio限速实战 | 2026-09-22 |
| Nginx WebSocket 与 SSE 并存:proxy_buffering 冲突排查 | 2026-09-22 |
| MySQL备份文件损坏怎么验证:一致性校验与恢复演练 | 2026-09-22 |