技术指南
systemd 服务重启循环:从退出原因到受控恢复
先止住 systemd 重启循环,再用退出结果、有效 unit、journal、core 与资源证据定位根因,完成受控恢复。
systemd 服务不断重启时,先阻止故障继续放大,再判断每次退出的真正原因。`Restart=` 只决定退出后是否重启,`StartLimitIntervalSec=` 与 `StartLimitBurst=` 只限制启动频率;两者都不能修复配置、权限、依赖、崩溃、watchdog 或资源问题。可靠恢复要把同一时间窗内的 unit 状态、退出结果、有效配置、journal 和业务症状对齐。
先把故障分到一条路径
| 观察 | 优先解释 | 下一步 |
|---|---|---|
| Result=exit-code,ExecMainStatus 非零 | 应用主动报错,常见于参数、配置、权限或依赖 | 读取该次退出前后的完整应用日志,并用服务身份核对输入 |
| Result=signal 或出现 core dump | 进程崩溃、被外部 signal 终止或触发断言 | 保存 signal、可执行文件版本、core 元数据和同窗部署变更 |
| Result=watchdog 或启动/停止 timeout | 通知协议、事件循环或生命周期钩子未在期限内完成 | 核对 Type=、WatchdogSec=、TimeoutStartSec= 与程序实现 |
| Result=oom-kill 或内核记录 OOM | unit 或宿主发生内存处置 | 对齐 cgroup 事件、内核日志和资源边界,不只提高 RestartSec |
| start request repeated too quickly | 启动次数撞到 rate limit | 先找前几次失败;限流是保护结果,不是根因 |
| 进程退出码为 0 但仍循环 | Restart=always/on-success、Type= 或前台/后台模型不匹配 | 核对主进程是否按 unit 类型保持预期生命周期 |
先保存状态、计数和时间窗
unit=example.service
stamp=$(date --utc --iso-8601=seconds)
printf 'incident_utc=%s\nunit=%s\n' "$stamp" "$unit"
systemctl status "$unit" --no-pager --full
systemctl show "$unit" \
--property=ActiveState,SubState,Result,ExecMainCode,ExecMainStatus \
--property=MainPID,NRestarts,Restart,RestartUSec \
--property=FragmentPath,DropInPaths
journalctl --unit="$unit" --boot=0 --since='30 minutes ago' \
--output=short-iso-precise --no-pager`systemctl status` 适合人工阅读,`systemctl show --property=` 适合保存稳定的键值证据。`Result` 描述最近一次服务结果,`ExecMainCode` 与 `ExecMainStatus` 保留主进程退出方式,`NRestarts` 是当前管理器周期内的重启计数。计数是现场线索,不是长期 SLA;管理器重启或 `reset-failed` 都会改变可观察状态。
读取真正生效的 unit,而不是只看一个文件
systemctl cat "$unit"
systemctl show "$unit" \
--property=FragmentPath,DropInPaths,User,Group,WorkingDirectory \
--property=EnvironmentFiles,ExecStart,Type \
--property=Restart,RestartUSec,StartLimitIntervalUSec,StartLimitBurst优先检查刚发生变化的层:部署包、drop-in、EnvironmentFile、服务用户、工作目录、路径权限、监听端口、DNS/TLS 和上游依赖。不要通过 `ps e`、`/proc/<pid>/environ` 或工单截图收集全部环境;它们可能含凭据。需要确认某个变量时,只验证名称、来源和是否存在,秘密值留在受控边界内。
确认 Type= 与进程生命周期一致
- `Type=simple` 或 `exec` 通常要求 ExecStart 进程保持前台运行;程序自行 daemonize 后主进程退出,systemd 可能把生命周期判断错。
- `Type=forking` 依赖传统后台化行为,PID 识别错误会让 systemd 追踪到错误进程;优先使用程序支持的前台模式。
- `Type=notify` 需要程序发送 READY=1;设置 WatchdogSec= 后还必须按约定持续发送 watchdog 通知。
- oneshot 任务的成功退出就是完成,不应借 `Restart=always` 把一次性动作伪装成长驻服务。
重启策略还会受到 `SuccessExitStatus=`、`RestartPreventExitStatus=` 和 `RestartForceExitStatus=` 影响。先明确哪些退出代表短暂故障、永久配置错误、正常停机和需要人工介入,再选择策略;不要把所有非零退出都无限重试。
把崩溃、watchdog 与 OOM 分开取证
since='2026-08-18 22:00:00 UTC'
until='2026-08-18 22:20:00 UTC'
journalctl -u "$unit" --since "$since" --until "$until" \
--output=short-iso-precise --no-pager
coredumpctl list --since "$since" --until "$until"
journalctl --kernel --since "$since" --until "$until" \
--output=short-iso-precise --no-pager \
| grep -Ei 'oom|killed process|segfault|watchdog'core dump 只证明进程异常终止的一个证据面,不能单独解释触发输入;没有 core 也不证明未崩溃,因为存储、大小、权限或容器边界可能禁用了采集。`Result=watchdog` 要回到程序通知实现和当时阻塞点,`Result=oom-kill` 要与 cgroup `memory.events` 和内核日志交叉验证。
重启策略要限制影响,而不是隐藏失败
| 设置 | 作用 | 设计问题 |
|---|---|---|
| Restart= | 按退出原因决定是否再次启动 | 永久配置错误、正常退出和瞬时依赖故障是否能区分 |
| RestartSec= | 每次自动重启前等待 | 依赖恢复时间、外部限流和告警响应需要多长 |
| StartLimitIntervalSec=/Burst= | 限制时间窗内允许的启动次数 | 达到上限后谁接管,是否避免持续冲击依赖 |
| RestartSteps=/RestartMaxDelaySec= | 较新 systemd 可逐步增加重启间隔 | 目标发行版版本是否支持,最大延迟是否符合恢复预算 |
| RestartPreventExitStatus= | 指定不应自动重启的退出码或 signal | 应用是否有稳定、已文档化的永久失败码 |
上游默认 `RestartSec=` 很短,直接沿用可能形成高频循环。具体等待和次数应来自依赖的恢复特性、任务幂等性、外部配额与告警响应时间。不要禁用 start limit 来追求“高可用”;没有退避和停止条件的自动恢复会把单点错误变成持续事故。
先静态检查,再做一次受控启动
sudo systemd-analyze verify /etc/systemd/system/example.service
sudo systemctl daemon-reload
sudo systemctl reset-failed "$unit"
sudo systemctl start "$unit"
systemctl show "$unit" \
--property=ActiveState,SubState,Result,ExecMainCode,ExecMainStatus,NRestarts
journalctl -u "$unit" --since='5 minutes ago' \
--output=short-iso-precise --no-pager`reset-failed` 会同时清除 failed 状态、start rate-limit 计数和服务 restart 计数,因此要在证据保存后执行。一次 `active` 也不等于恢复:某些服务会在启动后才连接依赖、加载数据或进入下一轮崩溃。恢复流量前先执行应用自己的只读健康检查,再逐步放量。
用稳定窗口和业务结果验收
- 在至少一个真实故障周期内,`NRestarts` 不再增长,Result、退出码和 journal 没有新失败。
- 真实健康检查、请求错误率、延迟、队列积压和依赖侧限流回到事先定义的范围。
- 服务使用预期的 FragmentPath、drop-in、用户、工作目录和部署版本,没有从旧文件或临时覆盖启动。
- CPU、内存、文件描述符、任务数和日志速率没有把故障转移到宿主或依赖。
回滚配置和版本,不回滚证据
变更前保存 unit、drop-in、应用版本和配置管理提交。新版本仍循环时,先 stop,恢复已知可用的应用与配置,执行 daemon-reload 和一次受控 start,再重复同一组业务验收。`systemctl revert` 可能移除本地 override 和 drop-in,只在明确理解发行版 unit 与本地定制边界时使用;更稳妥的是由配置管理精确恢复本次改动。
让下一次失败在失控前可见
监控 restart 计数增量、failed 状态、启动耗时、watchdog、OOM、日志速率和真实业务 SLO,并把发布版本与 unit 配置版本写入同一事件时间线。为瞬时故障设置有限重试、退避和人工接管;为永久配置错误设计明确退出码和不重试路径。发行版或 systemd 升级后重新核对新增指令与运行时属性,不能用 latest 手册替代目标主机版本。
本文命令在 2026-08-18 按 systemd 当前上游 service、unit、exec、systemctl、journalctl、coredumpctl、resource-control 与 systemd-analyze 手册完成语义和语法审阅,没有在读者的生产 unit 或状态型服务上执行。旧 systemd、用户级 unit、容器、发行版补丁和应用自身 supervisor 可能改变属性、路径与恢复顺序;执行前先确认 `systemctl --version`、本机手册和服务数据一致性要求。