交付给一线运维:一条服务重启了十几次、
systemctl start只回一句Job for app.service failed,journalctl里翻半天只有一行exited。其实 systemd 的失败信息非常规范,按五步走, 从"现象分类"到"读到根因",五分钟内能定位到具体那一行配置。
一、先分清三种"起不来"
不同失败形态,排查入口完全不同,先归类再动手:
- 根本起不来 :
systemctl start立刻返回失败,进程连创建都没成功。多为ExecStart路径、权限、解释器问题,退出码常见203/EXEC。 - 起来又马上退出 :能启动一秒,然后进程自己崩。表现为
Restart=always不断重启,状态里是activating (auto-restart),多半是应用自身配置或依赖问题。 - 起来了但状态不对 :
Active: active (running),可端口不通、健康检查失败。这是Type=与就绪判定不匹配,或进程 daemon 化后 systemd 跟丢了。
二、第 1 步:systemctl status 一眼读四行
bash
systemctl status app.service --no-pager
重点看四行:
Loaded:------ unit 文件路径是否正确,是否被masked、disabled。Active:------ 失败会直接标注(Result: exit-code)或(Result: timeout)。Process:------ 实际执行的命令行,以及status=203/EXEC这类关键退出码。- 末尾日志 ------ 只有最近 10 行,够用来判断方向,但不足以定根因。
203/EXEC 基本等于"这个命令 systemd 跑不起来":路径写错、没有执行权限、shelbang 坏了、脚本是 CRLF 换行。
三、第 2 步:journalctl -u 拿全量日志
bash
# 看这个服务最近 100 行
journalctl -u app.service -n 100 --no-pager
# 只看本次开机、只看错误级别
journalctl -u app.service -b -p err --no-pager
# 缩小到出问题的时间窗
journalctl -u app.service --since "10 min ago" --no-pager
三句高频报错的含义:
Unit app.service not found.------ 文件没放在/etc/systemd/system/,或放完没执行systemctl daemon-reload。Start request repeated too quickly.------ 重启太密被 systemd 熔断,需要systemctl reset-failed app.service再试。Address already in use------ 端口被别的进程占了,用ss -lntp找占用者。
四、第 3 步:核对 unit 文件本身
bash
systemctl cat app.service # 看最终生效内容(含 drop-in 覆盖)
systemd-analyze verify /etc/systemd/system/app.service # 语法与依赖检查
systemctl show app.service -p ExecStart -p Environment # 看 systemd 眼中的真实值
unit 文件里最容易写错的五处:
ExecStart必须写绝对路径 ,且不能带 shell 语法(管道、&&、重定向都要包一层bash -c)。Type=选错 。前台常驻用simple(默认),会 fork 后退出的老程序要forking+PIDFile,oneshot只跑一次不能常驻。EnvironmentFile=路径写错会让变量全空;写成EnvironmentFile=-/etc/app/env(前面加-)则文件缺失不报错。User=/Group=指定的账号对WorkingDirectory和日志目录没有写权限,进程会直接退出。- 依赖写反 :需要网络的要
After=network-online.target且Wants=network-online.target,只写After=network.target常常起太早。
改完 unit 文件必须:
bash
systemctl daemon-reload
systemctl restart app.service
五、第 4 步:查环境差异与资源限制
最常见的"我手工跑明明可以"------因为交互 shell 和 systemd 的环境完全不同:
- 不加载
/etc/profile:PATH只有极简几条,java、node找不到。解决:ExecStart写全路径,或在 unit 里显式Environment=PATH=/usr/local/bin:/usr/bin。 WorkingDirectory默认是/:相对路径配置一律找不到,必须显式指定。- 资源上限 :
LimitNOFILE、MemoryMax、TasksMax触顶会直接把进程打掉,journalctl里能看到 oom 或Failed to fork。 - 沙箱与 SELinux :
ProtectSystem=strict、PrivateTmp=true会让服务看不到宿主机路径,表现为"文件明明存在却报 no such file"。 - 看真实上限 :
systemctl show app.service -p LimitNOFILE -p MemoryMax。
六、第 5 步:排除端口与依赖冲突
bash
ss -lntp | grep -E ':8080|:3306' # 端口是否被占
systemctl list-dependencies app.service --reverse | head
如果服务启动后秒退,且日志停在"connecting to db...",那就是依赖的下游没起来。此时不要给应用加 Restart=always 硬扛------重启掩盖的是依赖顺序问题 ,应该在 unit 里用 After= / Requires= 声明清楚,或在应用层加重试与退避。
七、避坑清单
- 改完 unit 不
daemon-reload:改动完全不生效,白排查半天。 - 用
Restart=always掩盖根因 :日志被反复重启刷满,真正那一行错误被冲掉。先systemctl stop手动跑一次。 ExecStart里用 shell 语法 :管道、&&、$VAR都不被解析,必须bash -lc '...'。Type=forking却没配PIDFile:systemd 不知道主进程是谁,状态一直activating直到超时。TimeoutStartSec太短:大应用初始化要 2 分钟,默认 90 秒直接判失败,应显式放宽。- 脚本 CRLF 换行 :从 Windows 传上来的启动脚本会报
203/EXEC,dos2unix一下。 - 别在
/usr/lib/systemd/system/里直接改 :升级会被覆盖,自定义放/etc/systemd/system/,覆盖用 drop-in(/etc/systemd/system/app.service.d/override.conf)。
小结
口诀:看状态分类型 → 翻日志找报错 → 核 unit 查语法 → 比环境与限额 → 排端口与依赖。
systemd 的报错信息其实很诚实,它只是被"重启刷屏"和"环境不一致"藏起来了。 把服务先停掉手动跑一次、把 unit 文件当成代码来 review,绝大多数"起不来"都能一眼看破。