systemd 服务启动失败:区分单元配置、执行环境、权限与重启限流

8 次浏览1 条回复

适用于 Linux 上由 systemd 托管的服务出现 failed、启动后立即退出、status=203/EXECstart request repeated too quickly。命令基线为 systemd 245+、util-linux 2.36+;读取系统级单元、完整日志和其他用户文件通常需要 root。示例单元 app.service、程序路径和健康检查地址必须替换为实际值。先保留失败现场,不要一开始就循环重启、把服务改成 root,或放宽整个目录的权限。

1. 固定时间线与实际加载的单元

date -u
systemctl --version | head -n 1
systemctl status app.service --no-pager -l
systemctl show app.service \
  -p LoadState -p ActiveState -p SubState -p Result \
  -p ExecMainCode -p ExecMainStatus -p MainPID \
  -p FragmentPath -p DropInPaths
systemctl cat app.service
journalctl -u app.service -b --no-pager -o short-iso

systemctl cat 用于确认主单元和 drop-in 的合并来源,不能只查看某个猜测路径下的文件。日志应保留首次失败、后续自动重启和触发限流的完整顺序;只看最后一行容易把结果当成原因。若故障跨越了本次启动,按实际 UTC 窗口使用 --since/--until,并核对上一轮启动日志。

2. 先按退出方式分流

Result 描述单元结果,ExecMainCode 区分正常退出与信号终止,ExecMainStatus 才是退出码或信号编号。常见的 203/EXEC 指向执行阶段失败,200/CHDIR 指向工作目录切换失败;应再结合日志与本机的 systemd.exec(5),不要把所有非零状态都解释成应用自身返回。

若主进程确实开始运行后返回应用退出码,应转查应用日志、参数和依赖;若被信号终止,还要对齐内核日志、资源限制与 cgroup 状态:

journalctl -k --since '-15 min' --no-pager -o short-iso
systemctl show app.service \
  -p MemoryCurrent -p MemoryMax -p TasksCurrent -p TasksMax \
  -p LimitNOFILE -p OOMPolicy

这些属性的可用性会随 systemd 和 cgroup 版本变化,空值不能单独证明没有限制。

3. 检查语法、覆盖关系与命令解释方式

UNIT_PATH=$(systemctl show app.service -p FragmentPath --value)
test -n "$UNIT_PATH" && test -r "$UNIT_PATH" || exit 1
systemd-analyze verify "$UNIT_PATH"
systemctl cat app.service

systemd-analyze verify 可发现部分未知指令、依赖和命令问题,但仍需以当前管理器实际加载的主文件与 drop-in 为准。ExecStart= 不是交互式 shell 命令行,&&、管道、重定向和 shell 初始化文件不会自动生效。需要复杂逻辑时,应使用可审计的脚本并给出绝对路径;不要为了让操作符生效而无条件套一层 shell。

修改单元或 drop-in 后才执行 systemctl daemon-reload,然后重新查看 systemctl catsystemctl show,确认管理器确实加载了预期版本。

4. 复核服务用户、工作目录与执行链路

systemctl show app.service \
  -p User -p Group -p DynamicUser -p WorkingDirectory \
  -p RootDirectory -p RootImage -p PrivateTmp \
  -p ProtectSystem -p ProtectHome -p NoNewPrivileges

readlink -f /opt/app/bin/server
namei -l /opt/app/bin/server
stat /opt/app/bin/server
findmnt -T /opt/app/bin/server -o TARGET,SOURCE,FSTYPE,OPTIONS
head -n 1 /opt/app/bin/server

对脚本要同时核对首行解释器、解释器自身权限和每一级父目录的搜索权限;文件具有执行位仍可能被挂载参数 noexec、缺失解释器或访问控制阻止。WorkingDirectory= 也必须存在并允许服务用户进入。启用 RootDirectory=RootImage=ProtectHome= 或其他沙箱后,宿主机上存在的路径在服务命名空间内未必可见。

不要直接发布完整环境变量或环境文件内容,其中可能含敏感配置。服务不会继承交互式终端的 PATH 和 shell 初始化结果,程序与关键依赖优先使用明确的绝对路径。

5. 区分根因失败与重启限流

systemctl show app.service \
  -p Restart -p RestartUSec -p NRestarts \
  -p StartLimitIntervalUSec -p StartLimitBurst
journalctl -u app.service -b --no-pager -o short-iso

start request repeated too quickly 通常表示此前的连续失败已经触发启动速率限制,本身不是最早根因。应先修正第一处执行、权限、配置或应用错误。确认修正后,才使用:

systemctl reset-failed app.service
systemctl start app.service

reset-failed 会清除失败状态和相关计数;在取证前执行会损失一部分状态信息。若 Restart=always 使日志快速增长,可在获准的维护窗口内控制重试,但需要保留原配置并记录变更。

6. 从 systemd 状态验证到真实入口

systemctl status app.service --no-pager -l
systemctl is-active app.service
systemctl show app.service -p MainPID -p NRestarts -p ActiveEnterTimestamp
journalctl -u app.service --since '-5 min' --no-pager -o short-iso

active 只表示 systemd 认为单元处于活动状态,不等于应用已经可用。还应通过实际入口执行受控健康检查,例如已有 HTTP 健康端点时使用带超时的 curl,并核对监听、依赖连接和应用错误率。闭环标准是原失败入口恢复、观察窗口内 NRestarts 不再增长、没有新增退出记录,且服务以预期用户、参数和沙箱配置运行。

再补一个 203/EXEC 的窄分支:目标文件存在、权限链和挂载参数都正常时,还要检查二进制格式、ELF 解释器以及 LSM 拒绝。execve(2) 在动态加载器缺失时也可能返回 ENOENT,所以日志里的“文件不存在”未必指 ExecStart 本身。

命令前提:file 5.38+;readelf 来自 binutils 2.34+;读取完整内核或审计日志通常需要 root。

file /opt/app/bin/server
readelf -hW /opt/app/bin/server | sed -n '/Class:/p;/Machine:/p'
readelf -lW /opt/app/bin/server | sed -n '/interpreter/p'
uname -m
journalctl -k --since '-15 min' --no-pager -o short-iso \
  | grep -Ei 'apparmor|avc:|selinux|denied'

readelf 显示的解释器路径放到服务实际的 RootDirectory=/RootImage= 和挂载命名空间中核对;只在宿主机检查存在性不够。Machine 与主机架构不匹配、部署了错误制品、解释器未打进根目录,或 SELinux/AppArmor 拒绝执行,都可能表现为这一状态。若系统启用了 auditd,可再按同一 UTC 故障窗口查询 AVC 记录。

不建议用 ldd 检查来源不可信的可执行文件;这里用 readelf 只解析 ELF 元数据。修正后仍按原文流程 daemon-reload(仅单元有改动时)、reset-failed、启动并验证真实入口。