场景概述
systemctl start 失败时,systemd 给出的信息往往只有「退出码非 0」或控制进程退出,真正的根因要自己剥。失败集中在四层:单元定义本身(ExecStart 路径、语法、Type 判断)、运行环境(User、PATH、工作目录、环境变量、文件权限)、资源与依赖(端口占用、依赖未启动、内存与文件描述符上限、SELinux 拦截)、以及应用自身逻辑(配置错误、连不上数据库)。这套流程的目的,是把 systemd 当时的运行环境在终端里复现出来。
场景信息
| 触发条件 | systemctl start 报 Job failed,或服务启动后立刻进入 inactive/failed |
|---|---|
| 排查目标 | 定位到具体失败层,改对地方而不是反复重启碰运气 |
| 工具链 | status 看状态 → journalctl 看日志 → systemd-analyze verify 查语法 → 手动复现环境 → 逐项排除资源与权限 |
| 退出码读法 | 1 到 199 由应用自身返回;200 以上是 systemd 保留段,例如 203 表示无法执行命令(路径不存在、无执行权限、二进制格式不匹配),217 表示切换 User 失败 |
四个失败层
- 单元定义层:ExecStart 路径写错、脚本缺少可执行权限或 shebang、Type 与实际启动方式不符
- 运行环境层:User 不存在、WorkingDirectory 不存在或不可读、依赖的环境变量没在单元里声明
- 资源与依赖层:端口被占用、依赖服务未启动、文件描述符或内存上限过低、安全模块拦截
- 应用逻辑层:配置文件语法错误、证书路径不对、连不上数据库或上游服务
七步排查命令
七步排查命令
# 第 1 步:状态与退出码
systemctl status myapp --no-pager -l
systemctl show myapp -p ExecMainStatus,Result,NRestarts
# 第 2 步:完整日志
journalctl -u myapp -n 200 --no-pager
# 第 3 步:单元语法与依赖
systemd-analyze verify /etc/systemd/system/myapp.service
systemctl list-dependencies myapp | head
# 第 4 步:手动复现同环境
sudo -u appuser env -i PATH=/usr/bin:/bin /opt/myapp/bin/start --config /etc/myapp/app.conf; echo "exit=$?"
# 第 5 步:权限与路径
namei -l /opt/myapp/bin/start; ls -ld /opt/myapp /etc/myapp
# 第 6 步:端口冲突
ss -lntp | grep -E ':8080'
# 第 7 步:资源与安全模块
journalctl -u myapp | grep -iE 'oom|limit|denied|permission' | tail
ausearch -m avc -ts recent 2>/dev/null | tail # SELinux 拦截现象对照根因
| 现象 | 常见根因 | 定位命令 |
|---|---|---|
| status=203/EXEC | ExecStart 路径不存在、无执行权限、脚本缺 shebang 或有 Windows 换行符 | ls -l、file、head -1 查看脚本首行 |
| status=217/USER | 单元里的 User= 账号不存在 | id 用户名 |
| Address already in use | 旧实例未退干净,或被其他服务占用端口 | ss -lntp、ps -ef 双证确认 |
| 启动成功但立刻 inactive | Type 判断错误:前台进程 daemonize 后父进程退出,应改为 Type=forking | 对照应用启动方式与单元 Type |
| 日志里 Permission denied | WorkingDirectory 或日志目录属主不对,也可能是 SELinux 上下文 | namei -l 路径、ausearch -m avc |
| 反复重启后被拒绝启动 | 触发启动频率限制(StartLimitBurst) | systemctl reset-failed 后重新排查 |
systemd-analyze verify 只能检查单元语法、部分路径与依赖关系,不校验应用逻辑,通过它不代表服务能起来。它的价值在于快速排除「单元文件写错」这一层,避免在语法问题上绕圈。
手工用 sudo -u 复现要注意差异:systemd 会重置环境变量、设置独立的 cgroup、按 Type 判断启动是否成功。需要精确复现时用 systemd-run 启动一次临时单元:systemd-run --unit=test -p User=appuser -p WorkingDirectory=/opt/myapp /opt/myapp/bin/start,再用 journalctl -u test 看输出。
Restart=always 会让配置错误的应用无限重启,既掩盖真实日志又可能打满 CPU 与磁盘。排查阶段先 systemctl stop 停掉,确认根因后再启用 Restart=on-failure 并配合 StartLimitIntervalSec 与 StartLimitBurst 限制频率。
顺序上先廉价后昂贵:status 与 journalctl 是零成本的信息源,先把这两步看完再动手改配置。改完单元文件必须 systemctl daemon-reload 让 systemd 重新读取,否则启动的仍是旧定义;改完用 systemd-analyze verify 复检,再 restart 并立刻用 journalctl -u 单元名 -n 50 确认新日志里报错是否消失。
证据位置对照
| 现象 | 先看哪里 | 再看哪里 |
|---|---|---|
| 启动即失败 | 单元状态与执行错误码 | 路径与文件权限 |
| 运行中退出 | 退出码与标准错误 | 内核与资源记录 |
| 端口冲突 | 监听状态与进程列表 | 是否有残留实例 |
| 权限被拒 | 服务账号与文件属主 | 安全模块的审计记录 |
| 依赖不通 | 下游服务与网络状态 | 单元里的依赖声明 |
排查顺序遵循「先看单元再进应用」:单元层的错误码指向路径、权限、格式这类启动条件,应用日志回答业务逻辑为什么失败。顺序颠倒时容易在应用日志里翻很久却找不到任何线索,因为问题根本没走到应用代码那一层。