systemd 定时器 AccuracySec —— 没人量过的迟到

systemd 定时器 AccuracySec —— 没人量过的迟到

systemd 定时器迟到不是故障,是设计——AccuracySec 默认的 1min 留出一分钟窗口,把多个定时器的唤醒合并成一次 CPU 唤醒。实测附收据:闲机上 10 次计划触发变成 5 次,15 秒日历出现 26–34 秒间隔;同一定时器挨着繁忙的 2 秒邻居时,6 个时刻全部命中,最迟晚 119 ms。

HUMAN作者 @mrkeepalive40m3 分钟读完

你带着systemd 定时器 AccuracySec这个搜索而来:你的 OnCalendar=*:0/15 定时器总在每分钟第 7 秒触发,而 journalctl -u 却说一切正常。一切确实正常——是 systemd 故意挪走了你的唤醒。这篇文章就是量过的那一版:默认值买到什么、付出什么,以及把你的时间表拿回来的那一行。

要让 systemd 定时器严格按计划触发:在定时器的 [Timer] 小节里设置 AccuracySec=1s(或更低)。默认是 1 分钟——systemd 把每次唤醒对齐到一张粗网格上,最多可晚 60 秒触发,好把多个定时器合进一次 CPU 唤醒。用 systemctl show your.timer -p LastTriggerUSec 查真实延迟——它每次触发都会更新。

最后测试 2026-11-14 · systemd 262 (262.1-1-arch) · 全新 Arch VM。下面的每个定时器都真跑过;每张收据都逐字粘贴自那台机器。

闲机——1min 窗口,独跑 同一定时器——一个繁忙的 2 秒邻居 琥珀 = 真实触发 · 幽灵 = 刻度没人为它唤醒 CPU
图 1 —— 同一日历(*:*:00/15)、同一定时器、两台机器。闲机:十次刻度汇成五次触发——窗口吞掉了每个隔次的刻度。繁忙邻居:每次刻度都准点落地,因为窗口里本来就存在一次唤醒。

为什么我的 systemd 定时器会迟到?

论点句:默认配置的 systemd 定时器可能比计划时刻最多晚 60 秒才触发,而 list-timers 会毫无怨言地把它叫作“下一次到期”。不是 bug,也不是时钟漂移——是 AccuracySec,默认 1min,它存在的全部意义就是让调度器可以迟到。

$ sudo systemctl show accA.timer -p AccuracyUSec
AccuracyUSec=1min
$ systemd-analyze cat-config /etc/systemd/system.conf 2>/dev/null | grep -i DefaultTimerAccuracy
#DefaultTimerAccuracySec=1min

验证:逐字粘贴自实测 VM——原版 Arch,无用户配置,所以定时器报告的就是编译期默认值。注意属性拼写:unit 文件里写的是 AccuracySec=,systemd 262 把它暴露成 AccuracyUSec。

机制,一段说清:定时器的到期时刻到来时,systemd 并不立刻唤醒 CPU。它算出一个窗口——从计划时刻起,到 AccuracySec 之后为止——然后在窗口里挑一个能让它与其他定时器共用唤醒、或让 CPU 多睡一会儿的点触发。你的 *:0/15 日历说的是“每分钟四次”;调度器听到的是“四次拼车机会”——而在安静的机器上,它大约只会搭上其中一次。

实测下来,默认值能晚多少?

实验,一段说清:一个计划为 OnCalendar=*:*:00/15——每分钟四次预期触发——出厂精度的定时器,在同一台全新 Arch VM 上跑两遍。一次独占一台闲机,一次挨着一个繁忙的 2 秒定时器。每次触发都记录真实执行时间;收据就是原始时间戳。

先看闲机——大多数内务定时器实际生活的环境:

accA 1794634092.128787380
accA 1794634125.384544834
accCo 1794634125.384546104   ← co-timer mark was :30 of the previous minute
accA 1794634152.127459587
accCo 1794634186.135416303   ← co-timer mark was :30 of the previous minute
accA 1794634186.135424133
accA 1794634212.133508500

验证:10 个刻度过去,服务只跑了 5 次——在写着每 15 秒一次的日历上,间隔 26–34 秒(均值 30.0)。相对最近的刻度,每次触发分别晚 12.1、0.4、12.1、1.1、12.1 秒:半个日程,汇进五次唤醒。

同一定时器,同一日历——一个繁忙邻居(一个 2 秒定时器,AccuracySec=1us):

accA sched=1794633795101333 mono=193094591 exec=1794633795.114334868
accA sched=1794633795101333 mono=193094591 exec=1794633810.014311174
accA sched=1794633795101333 mono=193094591 exec=1794633825.116389742
  ⋮ (6 fires, 6 marks — every one on its mark)

验证:既然每 2 秒就排好一次唤醒,这些唤醒每一次都落在默认定时器的 60 秒窗口之内——于是调度器在第一个可用的唤醒上触发它:踩着刻度,最迟晚 119 ms。余量没有变小;它是被邻居的日程花掉了。

这一对收据就是整套机制的一屏速览:窗口是一个邀约,不是期限——systemd 可以在窗口内任意时刻触发,并且会在窗口覆盖到的最早一次 CPU 唤醒上触发。繁忙的机器:唤醒立刻就有,你的定时器显得很守时。安静的机器:没有现成的唤醒,调度器就把一次唤醒排在窗口的最远端,而窗口内排队的每次到期都搭这班车。闲置的笔记本和孤零零的服务器——大多数 cron 替代品运行的地方——正是安静的那一类。这就是为什么同一个 unit 会“在我桌面上没问题,到了 NAS 上就迟到”。

AccuracySec 实际改变的是什么?

同一日历,同一 VM,一行改动:AccuracySec=100ms。现在唤醒窗口只有十分之一秒宽,定时器落在日历指到的位置:

accB 1794634230.114374054
accB 1794634245.119370330
accB 1794634260.114446983

验证:三次触发,三个刻度——平均晚 116 ms,最迟晚 119 ms,窗口 100 ms;多出来的约 15 ms 是任何 VM 都有的调度损耗。这就是整颗旋钮:窗口宽度,仅此而已。

而合批本身在共跑定时器身上直接可见。在繁忙机器上以默认精度计划在 :30,它从没等满一个窗口——刻度过后 15 ms 就触发,搭的正是旁边 15 秒定时器本来就要做的那次唤醒。两个定时器,一次唤醒:

accCo sched=1794633810000832 mono=207994091 exec=1794633810.015037233
accCo sched=1794633810000832 mono=207994091 exec=1794633870.016986575
accCo sched=1794633810000832 mono=207994091 exec=1794633930.020077789
(accA exec at the same instants: 1794633795.114, 1794633810.014 …)

验证:co 的 :30 刻度同时也是 15 秒定时器的刻度——:30.000 的唤醒早已排好,co 晚 15 ms 搭了上去。在闲机上,即使没有 :30 邻居,它也玩了同一招:落在定时器 A 的唤醒上,误差不超过 1.2 µs(收据见上)。同一个窗口,更聪明的上车方式。

“systemd 不是迟到。它在拼车——一分钟一次唤醒,四位乘客。AccuracySec 决定这班车要坐多满才发车。”

怎样让定时器按计划触发?

一行 drop-in,除了定时器 unit 本身,别的什么都不用重启:

$ sudo systemctl cat accB.timer | grep -A2 '^\[Timer\]'
[Timer]
OnCalendar=*:*:00/15
AccuracySec=100ms
$ sudo systemctl daemon-reload && sudo systemctl restart accB.timer
$ sudo systemctl show accB.timer -p AccuracyUSec,LastTriggerUSec
AccuracyUSec=100ms
LastTriggerUSec=Sat 2026-11-14 05:33:45 UTC

验证:systemctl show 打印的是生效中的窗口——drop-in 压过编译期默认值,而且 LastTriggerUSec 每次触发都会更新。前后对比的收据就是一条查询,不必翻日志。

能压到多低:定时器 C 在 2 秒日历上跑 AccuracySec=1us,22 秒里触发 11 次,11 次中最差抖动 23 ms——亚毫秒级调度随时可得。但在全网铺开之前,先看看代价那一栏:

精度让你付出什么代价?

闲机用五次唤醒伺候了十个刻度;1 µs 的定时器在同样的窗口里会索要七十五次。在笔记本上,这正是默认值要替你省下的那笔开销——合批不是偷懒,它与把磁盘刷写归拢到一起是同一种功耗纪律。服务器无所谓;你的电池在乎。合理的档位:1s 给人能察觉的 deadline,1min(默认值)给察觉不到的内务活,低于 100ms 的一律不碰,除非你量过为什么——量过之后你会发现,一个带 sleep 循环的长驻服务胜过定时器蜂拥。至于笔记本上的 1us,那是把电池改成暖手宝的用法。

这颗旋钮,一眼看清

AccuracySec日历计划触发实际触发发生了什么
1min (default)*:*:00/15, idle box105间隔 26–34 秒,均值 30.0——触发次数减半
1min (default)*:*:00/15, busy neighbor66每个刻度都中,晚 ≤119 ms
100ms*:*:00/1533每个刻度都中,晚 ≤119 ms
1us*:*:00/21111每个刻度都中,晚 ≤22.7 ms
台架配方——在任何 VM 上复现这些收据

任何服务会记录自身启动时间的定时器都行:ExecStart=/bin/sh -c 'echo timer-run $(date +%%s.%N) >>/var/lib/stamps.log'(unit 文件里要转义 %%)。让一个 OnCalendar=*:*:00/15 的定时器以出厂精度在闲机上跑两分钟——数触发次数;然后加一个 *:*:00/2 的邻居,看着刻度一个个归位。逐次对比用 systemctl show your.timer -p LastTriggerUSec,它每次触发都会更新。服务里的 TRIGGER_TIMER_REALTIME_USEC/_MONOTONIC_USEC 环境变量携带的是定时器 unit 的激活时间戳,不是每次触发的计划时刻——别拿它们干这件事。

实测环境:一台全新 Arch VM(systemd 262, 262.1-1-arch, kernel 7.2.11-arch1-1)——同一个 *:*:00/15 定时器分别以闲机和挨着繁忙邻居两种方式运行,外加 100 ms 和 1 µs 两档精度;收据是原始的 date +%s.%N 时间戳,未作编辑逐字粘贴。上周那篇“journalctl 磁盘占用 —— vacuum 只会舀水,SystemMaxUse 关阀断流”(2026-11-13)里的 journald 上限检查,就是用一个与闲机场景一模一样的定时器跑的——这台台架就是这么挣到排期的。

人工实测台架 · systemd 262 (262.1-1-arch) · 全新 Arch VM · 最后测试 2026-11-14

一行配置定下你的时间表——AccuracySec=1s。一张收据证明它——LastTriggerUSec。