systemd 服务反复重启:从退出码、启动限速到资源约束的排查清单

15 次浏览2 条回复

适用于 Linux 上由 systemd 托管的服务反复进入 activating (auto-restart)failed,或出现 Start request repeated too quickly 的场景。命令基线为 systemd 245+;示例单元为 app.service,执行前应替换为实际单元名。先保留退出现场,不要一开始就反复执行 restart 或提高启动频率。

1. 固定时间线与最近一次退出结果

date -u
systemctl status app.service --no-pager -l
systemctl show app.service \
  -p ActiveState -p SubState -p Result -p MainPID \
  -p ExecMainCode -p ExecMainStatus -p NRestarts
journalctl -u app.service --since '-15 min' --no-pager -o short-iso

ExecMainCode=exited 时重点看退出码,killeddumped 时重点看信号、内核日志和是否生成了 core dump。NRestarts 是累计值,应结合 UTC 时间和日志判断它是否仍在增长;单次状态快照不能证明循环已经停止。

systemd 自身的特殊退出码也有定位价值。例如 200/CHDIR 常指向 WorkingDirectory=203/EXEC 常见于可执行文件不存在、不可执行、解释器路径错误或被访问控制策略拒绝。应用自定义退出码仍应以该程序文档为准。

2. 核对实际生效的单元与重启策略

systemctl cat app.service
systemctl show app.service \
  -p FragmentPath -p DropInPaths -p ExecStart \
  -p User -p Group -p WorkingDirectory -p EnvironmentFiles \
  -p Restart -p RestartUSec \
  -p StartLimitIntervalUSec -p StartLimitBurst

systemctl cat 会同时显示主文件和 drop-in,应以这里的合并来源为准,不要只看某个手头副本。逐项确认 ExecStart= 使用的绝对路径、服务用户、工作目录和环境文件都存在且权限匹配。systemd 的 ExecStart= 默认不经过交互式 shell,不能假定 shell profile、当前目录、PATH 展开或管道语法会自动生效。

Restart=always 会让正常退出也触发重启;Restart=on-failure 则应继续确认哪些退出码或信号被视为失败。StartLimitBurst 触发后,单元进入 failed 是保护结果,不是根因。仅增加限速阈值可能把高频崩溃变成持续资源消耗。

3. 排除资源限制与 OOM

systemctl show app.service \
  -p MemoryCurrent -p MemoryMax -p TasksCurrent -p TasksMax \
  -p LimitNOFILE -p OOMPolicy
journalctl -k --since '-15 min' --no-pager | \
  grep -iE 'out of memory|oom-kill|killed process'

若单元已退出,部分当前值可能显示为未设置,需与故障时的监控或日志结合。出现 OOM 记录时,应确认被终止的 PID 是否属于该单元,并区分单元级 MemoryMax=、系统整体内存压力和应用自身内存上限。文件描述符或任务数达到上限时,应用日志常伴随 EMFILEToo many open files 或无法创建线程;不要只放大限制,还要查连接、文件或线程为何持续增长。

4. 区分配置错误与应用启动失败

如果最近修改过单元文件,可对准备部署的文件做静态检查:

systemd-analyze verify /etc/systemd/system/app.service

路径必须替换为实际待检查文件。verify 能发现部分指令和依赖问题,但不能证明外部依赖、权限、端口或业务初始化一定成功。不要在服务仍可能运行时直接复制 ExecStart 到 root shell 执行:用户、环境、工作目录和隔离策略不同,而且可能启动第二个写入同一资源的实例。

若日志显示配置文件、证书、socket 或数据目录访问失败,应沿实际服务用户逐级检查路径权限,并同时核对 SELinux 或 AppArmor 的拒绝记录。遇到端口占用时,用 ss -lntpss -lxnp 确认监听者,不要先终止未知进程。

5. 修复后的受控恢复与闭环

只有在根因已确认、配置已准备好并处于允许变更的窗口时,才执行:

sudo systemctl daemon-reload
sudo systemctl reset-failed app.service
sudo systemctl start app.service

reset-failed 会清除 failed 状态和启动限速计数,因此应放在修复之后,而不是用来隐藏循环。随后至少验证:

date -u
systemctl is-active app.service
systemctl show app.service \
  -p ActiveState -p SubState -p Result -p MainPID -p NRestarts
journalctl -u app.service --since '-5 min' --no-pager -o short-iso

还应从真实调用路径执行一个无副作用的健康检查,并在超过原 RestartSec= 和典型崩溃周期后再次查看 NRestarts。只有服务持续为 active、主 PID 稳定、重启计数不再增长、日志无新增启动错误且业务检查成功,才能认为重启循环已经闭环。

可以再补一条“启动协议超时”分支:进程收到 TERMKILL 不一定是自身崩溃,也可能是 systemd 等不到启动完成或看门狗心跳。先读取实际生效值:

systemctl show app.service \
  -p Type -p Result -p PIDFile -p GuessMainPID \
  -p NotifyAccess -p TimeoutStartUSec -p TimeoutStopUSec \
  -p WatchdogUSec -p ExecMainCode -p ExecMainStatus
journalctl -u app.service --since '-15 min' --no-pager -o short-iso

Result=timeout,应结合单元类型排查:Type=notify 要确认应用在 TimeoutStartSec= 内发送 READY=1,启用 WatchdogSec= 后还要按约定发送 WATCHDOG=1Type=forking 则核对 PIDFile= 是否由守护进程及时、原子地写入,以及主 PID 是否判断正确。日志中若先出现 start operation timed outwatchdog timeout,随后才收到终止信号,根因通常在启动协议或时限,而不是退出码本身。

修复后除观察 NRestarts,还应至少等待超过一个 TimeoutStartSec= 或多个 WatchdogSec= 周期,再确认单元持续为 active 且日志没有新的超时记录。上述属性适用于文中 systemd 245+ 基线;未启用对应机制时通常显示为 0 或空值。

还应检查“退出状态映射”是否改写了 Restart= 的表面语义。即使同样是退出码 0、非 0 或某个信号,以下指令也可能让 systemd 将其视为成功、禁止重启或强制重启。对文中的 systemd 245+ 基线,可先只读取实际生效值:

systemctl show app.service \
  -p Restart -p SuccessExitStatus \
  -p RestartPreventExitStatus -p RestartForceExitStatus \
  -p ExecMainCode -p ExecMainStatus -p Result
systemctl cat app.service

SuccessExitStatus= 会把额外的退出码或信号纳入成功集合;RestartPreventExitStatus= 命中时,即使 Restart=on-failurealways 也不会自动重启;RestartForceExitStatus= 则可让原本不触发重启的状态进入重启流程。因此,看到“失败但没有重启”或“正常退出却继续重启”时,不应只依据 Restart= 下结论,还要在主单元和所有 drop-in 中查这三项的来源。

修正配置并在允许的变更窗口完成 daemon-reload 后,可让服务走一次可控的预期退出路径,再用下面的只读检查闭环:

systemctl show app.service \
  -p ActiveState -p SubState -p Result \
  -p ExecMainCode -p ExecMainStatus -p NRestarts
journalctl -u app.service --since '-5 min' --no-pager -o short-iso

验证重点是实际退出状态是否与配置中的状态集合匹配,并在超过一个 RestartSec= 周期后确认 NRestarts 的变化符合预期。生产服务不宜为了验证而注入非预期故障。