重启决策取决于退出分类、等待时间与启动速率限制。

systemd 重启策略要明确失败与限流规则

重启策略决定当前进程结束后,服务管理器是否应该再启动一个进程。它可以帮助恢复短暂故障,但修不好缺失配置或必然重复的程序错误。一个有用的契约需要分别回答三个问题:哪些结束方式应该重试、重试前等多久、一个时间窗口内允许启动多少次。这些设置各管一件事,不能把它们笼统理解为“失败后会自己恢复”。 坏掉的样子 Restart=on-failure 不代表任何信号结束都会重启。对于 oneshot 以外的服务类型,通常的 SIGHUP、SIGINT、SIGTERM 和 SIGPIPE 终止,默认被视为干净结束;非零退出码,以及 SIGKILL 这样的异常信号,才符合默认失败规则。SuccessExitStatus 和专门的重启例外指令还能改变分类,因此要检查最终生效的单元,不能只看其中一行策略名字。 on-abnormal 也不是 on-failure 的“更严格同义词”:它排除了普通的非零进程退出码,保留异常信号以及部分超时或看门狗故障。选择依据应是程序怎样报告可恢复失败与主动结束。如果进程始终存活,却已经不再提供有用工作,还需要单独配置发现这种状态的机制;基于退出的重启逻辑不会自动推断业务是否健康。 RestartSec 提供下一次重启前的等待时间。在下面的 systemd 252 实验里,它是固定延迟,不是越来越长的指数退避。StartLimitIntervalSec 和 StartLimitBurst 限制窗口内的启动次数,手动启动也算在内;它们既不是整个生命周期的重试预算,也不会在超限后自动预约下一窗口边界再尝试一次。限流与恢复触发应分别理解。 快速验证 在一次性的 Linux systemd 环境中,以管理员身份运行这个 Python 脚本。它创建带有唯一名字的运行时单元和临时工作程序,完成后删除。实测环境为 Debian 12、systemd 252.39。工作程序起初每次都以状态七退出,同时记录每次真实调用: from pathlib import Path from tempfile import TemporaryDirectory import subprocess,time,uuid unit='xbcatm-restart-lab-'+uuid.uuid4().hex[:8]+'.service' path=Path('/run/systemd/system')/unit assert not path.exists() def ctl(*args): result=subprocess.run(['systemctl',*args],text=True,capture_output=True,timeout=10) assert result.returncode==0,(args,result.stderr) return result.stdout.strip() def value(key):return ctl('show',unit,'-p',key,'--value') def wait_for(check): deadline=time.monotonic()+5 while not check(): assert time.monotonic()<deadline,'state transition timed out' time.sleep(0.02) with TemporaryDirectory(prefix='xbcatm-restart-',dir='/run') as directory: root=Path(directory);worker=root/'worker.py';calls=root/'calls';healthy=root/'healthy' worker.write_text("""from pathlib import Path import sys,time root=Path(__file__).parent with (root/'calls').open('a') as output: output.write('started\\n') if not (root/'healthy').exists(): sys.exit(7) time.sleep(60) """) path.write_text(f'''[Unit] StartLimitIntervalSec=10s StartLimitBurst=3 [Service] Type=exec ExecStart=/usr/bin/python3 {worker} Restart=on-failure RestartSec=100ms ''') try: ctl('daemon-reload');ctl('start',unit) wait_for(lambda: value('ActiveState')=='failed') assert calls.read_text().splitlines()==['started']*3 assert value('Result')=='exit-code' subprocess.run(['journalctl','--sync'],check=True) logs=subprocess.run(['journalctl','-u',unit,'--no-pager','-o','cat'],capture_output=True,text=True,check=True).stdout assert 'Start request repeated too quickly' in logs print('three starts; failed/exit-code with start-rate-limit journal message') healthy.touch();ctl('reset-failed',unit);ctl('start',unit) wait_for(lambda: len(calls.read_text().splitlines())==4) first=value('MainPID');assert first!='0' ctl('kill','--kill-who=main','--signal=TERM',unit) wait_for(lambda: value('ActiveState')=='inactive') assert value('NRestarts')=='0' print('on-failure treats ordinary SIGTERM as clean; no restart') ctl('start',unit);first=value('MainPID') ctl('kill','--kill-who=main','--signal=KILL',unit) wait_for(lambda: value('MainPID') not in ('0',first)) assert value('NRestarts')=='1' print('SIGKILL triggers one restart') ctl('stop',unit);time.sleep(0.2) assert value('ActiveState')=='inactive' print('explicit stop remains stopped') finally: ctl('stop',unit) if value('ActiveState')=='failed':ctl('reset-failed',unit) path.unlink();ctl('daemon-reload') 第一阶段允许十秒内启动三次,自动重启间隔设得很短且固定。程序自己写出的文件恰好包含三项记录,所以这里统计的是实际进程启动,而不是先猜某个管理器计数器的含义,再把那个数字当作执行证据。之后单元留在失败状态,日志明确说明启动请求过于频繁,新的启动被速率规则挡住。 ...

十一月 7, 2027 · 1 分钟 · XB Field Notes