定时器控制后续激活,服务单元记录实际执行结果。

systemd timer 安排服务启动,不保证任务成功

systemd timer 把调度规则与执行工作的服务分开。定时器决定何时请求激活,服务则定义命令、执行身份、环境、超时和结果。因此,维护任务可以使用普通单元工具检查,但一个按时发出的启动请求,并不能证明备份、清理或报表已经正确完成。调度是否运转与工作是否成功,需要不同证据;只有两者都符合要求,周期任务才真正达到目的。 坏掉的样子 活动的定时器可以与失败的服务同时存在。如果监控只检查 timer 是否活动,就会漏掉真正执行工作的失败。同样,最近触发时间只说明调度发生过,不代表业务输出成功。应检查服务结果,以及任务本来要生成的产物或状态变化,并保留足够上下文区分前后两次激活,避免把昨天留下的文件误当成今天任务已经完成。 除非通过 Unit 指向别的目标,定时器通常激活同名的 service。如果目标服务已经活动,就不会再启动另一份独立副本。这阻止的是同一个单元重叠激活,不是分布式锁,也不能阻止另一个服务执行相同命令。oneshot 如果使用 RemainAfterExit=yes 保持活动,后续计划执行就可能一直不再运行命令;保留状态是否合适,取决于任务真正需要表达什么。 停止定时器会停止后续调度,但不会自动停止已经开始的服务调用。事故期间暂停维护工作时,这个区别尤其重要:需要检查目标服务,再有意决定让当前工作结束还是中断它。即使停止命令成功,先前已经发生的文件或数据库修改也不会因此自动回滚,因此不能把“停了 timer”当成系统已经没有维护副作用。 快速验证 在 PID 1 为 systemd 的一次性 Linux 系统中,以管理员身份运行下面的 Python 脚本。实测环境是 Debian 12、systemd 252.39。它创建唯一命名的定时器与服务,以及临时工作程序。工作程序记录真实进程进入和完成时刻,成功任务则通过显式释放文件等待,以便直接观察调度行为: from pathlib import Path from tempfile import TemporaryDirectory import json,subprocess,time,uuid name='xbcatm-timer-'+uuid.uuid4().hex[:8];service=name+'.service';timer=name+'.timer' paths={unit:Path('/run/systemd/system')/unit for unit in (service,timer)} assert all(not p.exists() for p in paths.values()) def run(*args): result=subprocess.run(args,text=True,capture_output=True,timeout=10) assert result.returncode==0,(args,result.stderr) return result.stdout.strip() def value(unit,key):return run('systemctl','show',unit,'-p',key,'--value') def wait_for(check): deadline=time.monotonic()+5 while not check(): assert time.monotonic()<deadline,'transition timed out' time.sleep(0.02) with TemporaryDirectory(prefix=name+'-',dir='/run') as directory: root=Path(directory);worker=root/'worker.py' worker.write_text(r'''from pathlib import Path import json,sys,time root=Path(__file__).parent;number=len(list(root.glob('start-*.json')))+1 def record(kind): temp=root/'record.tmp';temp.write_text(json.dumps(time.monotonic())) temp.replace(root/(kind+'-'+str(number)+'.json')) record('start') if (root/'fail').exists():sys.exit(7) while not (root/('release-'+str(number))).exists():time.sleep(0.01) record('end') ''') paths[service].write_text(f'[Service]\nType=oneshot\nTimeoutStartSec=10s\nExecStart=/usr/bin/python3 {worker}\n') definition='[Timer]\nOnActiveSec=100ms\nOnUnitInactiveSec=10s\nAccuracySec=1ms\n' paths[timer].write_text(definition);(root/'fail').touch() try: run('systemd-analyze','verify',*[str(p) for p in paths.values()]) run('systemctl','daemon-reload');run('systemctl','start',timer) wait_for(lambda:value(service,'ActiveState')=='failed') assert value(service,'ExecMainStatus')=='7' and value(timer,'ActiveState')=='active' run('systemctl','stop',timer) print('timer active while its first job failed with exit status 7') (root/'fail').unlink();run('systemctl','reset-failed',service) paths[timer].write_text(definition.replace('OnUnitInactiveSec=10s','OnUnitInactiveSec=250ms')) run('systemctl','daemon-reload');run('systemctl','start',timer) wait_for(lambda:(root/'start-2.json').exists()) time.sleep(0.65) assert len(list(root.glob('start-*.json')))==2 (root/'release-2').touch();wait_for(lambda:(root/'start-3.json').exists()) gap=json.loads((root/'start-3.json').read_text())-json.loads((root/'end-2.json').read_text()) assert gap>=0.25,gap print('no overlapping launch; next job starts at least 250ms after prior deactivation') pid=value(service,'MainPID');assert pid!='0' run('systemctl','stop',timer) assert value(service,'MainPID')==pid and value(service,'ActiveState')=='activating' print('stopping timer leaves its already running oneshot alive') (root/'release-3').touch();wait_for(lambda:value(service,'ActiveState')=='inactive') time.sleep(0.4) assert len(list(root.glob('start-*.json')))==3 and value(service,'Result')=='success' print('released job completes; stopped timer schedules no further run') finally: run('systemctl','stop',timer,service) for unit,path in paths.items(): if value(unit,'ActiveState')=='failed':run('systemctl','reset-failed',unit) path.unlink() run('systemctl','daemon-reload') 第一次触发的工作进程故意以状态七退出,服务变为失败,定时器却继续活动。此时相对服务结束的下一次间隔先设置为十秒,让脚本有时间检查这个状态,并在再次尝试前停止定时器。结果证明调度器完全可能正常工作,而它安排的业务任务一直失败;仅凭 timer 状态不能得出任务可靠的结论。 ...

六月 21, 2027 · 2 分钟 · XB Field Notes