SYSTEMD · 2026-11-03

用 systemd 让服务每天自动重启,别再手写 cron

让 systemd timer 每天重启服务,别再用 cron 硬凑:OnCalendar=daily、RandomizedDelaySec、Persistent,错峰、断机补跑都自带,凌晨零点不再千机踩踏,磁盘不必硬扛。systemd 262 实测:95 秒内三次触发,数据全在文中。

HUMAN作者 @mrkeepalive1h2 分钟读完

timer 是 systemd 自带的定时重启方案。OnCalendar=daily 定在零点,RandomizedDelaySec 把负载摊到零点之后,Persistent=true 负责停机后补跑。cron 也能做同样的重启——但它是瞎跑:卡在零点整,和机器上所有定时任务挤在一起,而且完全不记录上次什么时候跑过。今晚的实测中,timer 在 95 秒内触发了三次,日志把每一次都标到了十分之一秒。

每日重启的配方长什么样?

两个文件。一个只会一招——重启应用;另一个决定什么时候。谁都不碰应用自己的单元。

# /etc/systemd/system/app-restart.service
[Unit]
Description=scheduled restart of app

[Service]
Type=oneshot
ExecStart=/usr/bin/systemctl try-restart app.service

# /etc/systemd/system/app-restart.timer
[Unit]
Description=restart app once a day

[Timer]
OnCalendar=daily
RandomizedDelaySec=45m
Persistent=true

[Install]
WantedBy=timers.target

验证:sudo systemctl daemon-reload && sudo systemctl enable --now app-restart.timer && systemctl list-timers app-restart.timer——NEXT 列有了值,就说明调度权已经归 timer 了。

用 try-restart,别用 restart:万一应用恰好停着,普通 restart 会悄悄把它拉起来——可能复活你特意停掉的东西。try-restart 只碰已经在跑的。那些绝不允许停着的服务换成 restart 就好,睡觉都安稳。

OnCalendar=daily日历轮盘 · 00:00 RandomizedDelaySec最多 45 分钟,每次触发现抽 Persistent=true戳记 · 错过的日子开机补跑 oneshot · try-restart仅当应用正在运行 应用归来,焕然一新同一单元,新 PID
五个活动部件,一天一次重启。戳记是磁盘上唯一的状态——一个零字节文件。

为什么不干脆用 cron?

因为 cron 这根拐杖撑一次,之后就全靠你扶着。@daily systemctl restart app 在 root 的 crontab 里就一行——问题紧跟着就来了:上次什么时候触发的?机器关着的时候它到底跑没跑?为什么全机器的任务都挤在 00:00:00 一起盖章?cron 的 man 页把 @daily 定义为零点整;这样排程的每个服务都在同一秒挤同一个闸机,第一个被撞上的就是磁盘。

老实账本:cron 重启应用,仅此而已。没有 list-timers 列,磁盘上没有戳记,停机后不会补跑,journal 里也没有对应单元可查——前提还是装了 cron 守护进程。我们实测机的底层镜像里压根没有。而在现代 Arch 机器上,timer 们已经在 PID 1 手下待命。

问题cron @dailytimer
上次什么时候触发?grep 日志,碰运气list-timers,LAST 列
零点时机器关着?静默跳过Persistent 开机补跑
到底几点执行?00:00:00 整00:00 加上抽中的延迟
要不要多养守护进程?cron 或 crond不用——PID 1 已经代劳
日志在哪?发邮件,如果有journalctl -u,结构化

一般情形——timer 何时胜过一行 cron、为什么——在我们之前那篇 cron 对决 systemd timer:cron 跑命令行,timer 跑单元里。这篇只是「每晚重启」这个特例。

怎么避免零点的千机踩踏?

一行搞定:RandomizedDelaySec=45m。每次日历到期,timer 会现抽一个随机延迟,于是一打夜间重启散落在窗口各处,而不是挤在同一个瞬间。实测跑的是分钟级缩小版配方——日历定在 :30,延迟上限 20 秒——结果每一次抽取都落在不同位置:

实测原始收据 —— Arch 上的 systemd 262
Nov 03 05:13:48.974739 bench-arch systemd[1]: Starting scheduled restart of app...
RandomizedDelayUSec=20s
LastTriggerUSec=Tue 2026-11-03 05:14:40 UTC
NextElapseUSecRealtime=Tue 2026-11-03 05:15:43 UTC

从行里读算术:日历 30,上限 20——三次分别抽到 18.97、10.42 和 13 秒。三次触发,三个不同的秒位,全在上限之内。cron 不会抽签:它的 man 页永远把你钉在固定的那一秒。调回 45m,夜间重启就会摊满整个窗口,磁盘再也不用迎接人潮。

重启时刻机器正好关着怎么办?

Persistent=true 就是补跑条款。每次触发后,timer 会碰一下零字节戳记——/var/lib/systemd/timers/stamp-app-restart.timer——开机时的 systemd 拿它和日历对账。机器关了一整夜,早上 09:00 开机:错过的重启立刻补跑一次。没有这行,错过的触发就静默跳过,应用要多等一整天才有 reset。戳记是一个零字节的承诺:下一次开机后,我欠你一次重启。

实测机上的戳记目录,原样未动
-rw-r--r-- 1 root root 0 Nov  3 05:13 stamp-app-restart.timer
-rw-r--r-- 1 root root 0 Oct 24 10:42 stamp-archlinux-keyring-wkd-sync.timer

第二行不是我们的:底层镜像自带这个 timer,已经启用、已经盖了戳。这个模式正是默认调度器的习惯——Arch 的 keyring 更新也是这样在停机后补跑的,你的应用将来一样。

Restart=always 不就是干这个的吗?

不是,把两者搞混正是「零点宕机」这种故事的由来。Restart=always 由失败驱动:主进程退出,systemd 在 RestartSec 后把它再拉起来。只要应用健康运行,它永远不会触发——没有可反应的东西。它做不了日历维护,因为健康不是一个日期。timer 由日历驱动:00:00 一到,重启就执行,健康与否无关。两个都留,各管各的——Restart=always 管崩溃,timer 管夜间复位。它们盯的是不同的钟,永远不会打架。

怎么确认它真的触发过?

systemctl list-timers app-restart.timer --no-pager
journalctl -u app-restart.service --since today --no-pager

验证:过了零点,NEXT 会为明天重新填上,LAST 则落在你的延迟窗口内。关键的那行日志是 Starting scheduled restart of app...——后面跟着应用自己的停止与启动:

实测触发前后应用自己的日志,原样未动
Nov 03 05:13:48.990989 bench-arch systemd[1]: Stopping the app that gets restarted nightly...
Nov 03 05:13:48.993916 bench-arch systemd[1]: Started the app that gets restarted nightly.

每晚重启是一份自白:有什么东西在泄漏,只是还没人找到那个洞。

今晚就按这份清单把服务切过去,让 timer 背着这份自白,直到有人真正修好泄漏——然后删掉两个单元,让机器睡过零点。这就是退出条件,戳记文件也不会想你。

实测方法,原始收据自己看

一次性 Arch 虚拟机(quickemu cloud 镜像,不动宿主机):跑一个执行 sleep infinity 的 app.service,把 timer 配方缩到 OnCalendar=*-*-* *:*:30 加 RandomizedDelaySec=20,让一次触发落在一分钟等待之内,然后盯着日志看 95 秒。三次触发、戳记文件、list-timers 各列和 systemd 版本都在同一份原始日志里,上文原样印出。第三次触发的时钟是 VM 开机时还没走稳的那只;它的算术是全文唯一不替其辩护的一行,日志原样保留。