用 systemd timer 代替 cron 跑周期任务

2026-11-02 · 运维

我有一台免费沙盒机器,规则是「30 分钟没有 SSH 会话就挂起」。 最初保活脚本挂在 cron 上,结果经常出现该跑的时候没跑—— 机器挂起期间 cron 不会补,醒来之后它只等下一个整点,中间那段就空着了。

cron 的问题不在于能不能跑

cron 的语义是「到点了,如果机器醒着,就执行」。机器没醒,这一次就永久错过了, 没有任何补偿机制,也没有地方记录「我本该跑但没跑」。 对于保活、心跳、证书续期这类任务,漏一次可能就要等很久才发现。

systemd timer 有 Persistent=true,配合 OnCalendar= 使用时, 会把「上次实际触发时间」记在 /var/lib/systemd/timers/ 下。 开机或从挂起恢复后,如果发现错过了一个触发点,会立刻补跑一次。

最小可用配置

两个文件:一个定义要做什么,一个定义什么时候做。

1. service:只负责执行

[Unit]
Description=keepalive ping
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/keepalive.sh
# 失败不无限重试,避免和下一次触发叠在一起
StartLimitIntervalSec=0

2. timer:负责调度

[Unit]
Description=keepalive ping every 10 minutes

[Timer]
OnCalendar=*:0/10
Persistent=true
# 随机延迟,避免多台机器同一秒打同一个接口
RandomizedDelaySec=30

[Install]
WantedBy=timers.target

启用和检查:

systemctl daemon-reload
systemctl enable --now keepalive.timer

# 看下次触发时间和上次结果
systemctl list-timers keepalive.timer
systemctl status keepalive.service

几个实际踩到的点

Type=oneshot 是必须的

周期任务基本都是「跑完就退出」的脚本,用默认的 Type=simple 时, systemd 会认为主进程一启动就进入运行态,脚本退出后状态显示为 failed 的情况很难读。 明确写 Type=oneshot,退出码就是结果。

日志默认就在 journald 里

不需要在脚本里自己重定向到文件。用 journalctl -u keepalive.service -n 50 就能看到最近几次的输出, 而且自带时间戳和退出码。要长期保留的话再配 Storage=persistent。

OnCalendar 的语法容易记错

写法含义
*:0/10每 10 分钟(整 10 分触发)
*-*-* 04:00:00每天凌晨 4 点
Mon *-*-* 09:00每周一 9 点
hourly / daily内置别名,等价于整点 / 每天 0 点

不确定的时候用 systemd-analyze calendar '*:0/10' 验证, 它会直接打印出接下来几次的触发时间。

什么时候仍然适合用 cron

如果机器是常驻的、任务对漏跑不敏感、或者只是临时跑个一次性脚本, cron 依然更省事——一行就够,不用建两个 unit 文件。 但只要涉及「不能漏」和「机器可能不在线」,我现在的默认选择是 timer。

← 返回笔记列表