Linux下systemd服务启动失败与反复重启的排查方法(status=203、217退出码、start-limit-hit与journalctl定位)

执行systemctl start之后跳出一行「Job for app.service failed because the control process exited with error code.」,后面跟一句「See "systemctl status app.service" and "journalctl -xe" for details.」。这两句等于没说,真正的死因在status输出的中间几行,以及journal里更早的日志。按实际排查顺序走一遍。

status输出该看哪几行

systemctl status app.service的输出分三块:Loaded那行给出unit文件的实际路径与preset状态,路径不是刚改的那个文件时,说明systemd读的是另一份;Active那行形如Active: failed (Result: exit-code),后面挂着的status=203/EXEC才是重点;再往下Process行给出ExecStart的完整命令行与退出码。

status后面斜杠左边是数字,右边是systemd给的常量名,比纯数字好记。

实测遇到过的几种:203/EXEC是执行不了,200/CHDIR是工作目录不存在,217/USER是User=写了个系统里没有的账号,1/FAILURE是程序自己退了非零,126与127分别是文件不可执行、命令没找到。这几个数字别混着看,203和127的排查方向完全不同。

203/EXEC的四个成因,按出现频率排。一是ExecStart=必须是绝对路径,写ExecStart=app这种光秃秃的命令名,systemd只在编译时固定下来的那几个目录里找,/opt/myapp/bin永远不会在列表里。二是文件没有可执行位,chmod +x补上。三是脚本首行shebang指的解释器不存在,写#!/usr/local/bin/python3.11而机器上装的是3.10,报的也是203而不是127。四是SELinux上下文不对,ls -Z看一眼,再用restorecon或semanage fcontext修,CentOS与RHEL上这条占了相当一部分。之前有台机器从/root下mv了一个脚本到/opt,权限、shebang全对,就是起不来,最后卡在context上是type=default_t而非bin_t。

systemctl status app.service -n 0
ls -l /opt/myapp/bin/app
head -1 /opt/myapp/bin/app
ls -Z /opt/myapp/bin/app
journalctl -t setroubleshoot -b

systemd-analyze verify能在真正启动之前把上面这四类问题报出来,改完unit文件先跑它,比反复启停省事:

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

这条命令在systemd 249.11、255.4,以及Debian 13上的257都验证过,输出格式一致。它不检查运行时的问题(端口冲突、连不上数据库),只管unit文件本身。

journalctl -u会漏掉最关键那行

这是最容易白折腾的地方。systemd自己记的那些失败行(「Start request repeated too quickly」「Failed with result 'start-limit-hit'」)是PID 1写的,它们的_SYSTEMD_UNIT字段是systemd自己的单元名,服务名放在另一个叫UNIT的字段里。journalctl -u app.service内部两个字段都查,所以看得到;但拿-o json导出来按_SYSTEMD_UNIT过滤,恰好就把失败原因那几行滤掉了。

另外「Failed with result」这行有个固定的MESSAGE_ID,值在所有systemd机器上都是d9b373ed55a64feb8242e02dbe79a49c,比按文本grep稳。而「Failed to start」这个文本反而不靠得住:Type=simple的服务在二进制文件报错之前就已经被判定为启动成功了,那行压根不会出现。

journalctl -u app.service -b --no-pager -n 50
journalctl -b -o json | jq -r 'select(.MESSAGE_ID=="d9b373ed55a64feb8242e02dbe79a49c") | [.UNIT,.UNIT_RESULT]|@tsv'

start-limit-hit不是原因,是结果

Active那行变成failed (Result: start-limit-hit)时,很多人第一反应是调参数。其实systemd只是把启动次数拦住了:默认StartLimitBurst=5、StartLimitIntervalSec=10s,也就是10秒内最多启5次。配了Restart=always的服务崩了,RestartSec默认100ms,5次重启0.5秒就打完,然后被判start-limit-hit。

这时候status显示的最后几行全是systemd自己的限速日志,程序真正的报错被顶到上面去了。往上翻,或者journalctl -u app.service -b -g 'Failed with result'直接搜。把根因修掉之后,还要清一次计数器,否则手工start也会被拒:

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

reset-failed只清计数,不治病。没修根因就清,会立刻回到同一个循环里。

想让重试有喘息空间,别去把StartLimitIntervalSec改成0(那就是无限重启),而是把RestartSec拉长到让5次重启塞不进10秒窗口。3秒就够:5次耗时15秒,超过窗口,正常抖动扛得住,真死循环照样被拦。这两个参数要写在[Unit]段,写进[Service]段新版本systemd会提示Unknown key name并忽略。

systemctl edit app.service
[Unit]
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Restart=on-failure
RestartSec=5s

systemctl edit会生成/etc/systemd/system/app.service.d/override.conf并自动reload,比直接改/usr/lib/systemd/system下那个文件强,后者会在下次包升级时被覆盖。手工改任何unit文件之后必须systemctl daemon-reload,不然systemd还在用内存里那份旧的,status里会带一句changed on disk的提示。

Type=与超时

TimeoutStartSec默认90秒,但只有部分Type会真的等。Type=simple不等待任何信号,也就永远不会因启动超时失败;Type=forking等父进程退出;Type=notify等程序发READY=1;Type=dbus等总线名。把一个不会说notify协议的程序标成Type=notify,表现就是每次启动卡到90秒然后被kill,然后被Restart=拉起来,再卡90秒,几轮之后落到start-limit-hit。

现场见过一次是Java服务,程序本身40秒才初始化完,标了Type=notify但压根没引systemd的notify库。改回Type=simple,顺手把TimeoutStartSec放宽到180,问题就没了。慢启动的程序与其硬等,不如让它在真正listen之后再通知systemd。

沙箱指令与环境变量

手工跑得好好的,交给systemd就失败,十有八九是沙箱或环境。ProtectSystem=strict会把整个文件系统挂成只读(full是/usr与/etc,yes是/usr与/boot),要写的地方必须用ReadWritePaths=逐条放开;PrivateTmp=yes给服务一个独立的/tmp,写在/tmp下的pid文件或者socket对方进程就找不到;NoNewPrivileges=yes挡掉提权;WorkingDirectory=不写的话工作目录是/,相对路径全废,目录不存在则报200/CHDIR。

EnvironmentFile=指定的文件不存在会直接启动失败,前面加个减号(EnvironmentFile=-/etc/app.env)可以让它缺失也不报错。文件里不做命令替换,写成$(...)就是一个字面串,字面量美元符号要写成两个美元符号。验证环境变量到底进没进去:

systemctl show app.service -p Environment -p EnvironmentFiles
sudo -u appuser /opt/myapp/bin/app

第二条是按unit里配的User=原样复现一次,权限、依赖、超时问题基本都能在这行暴露出来,比看日志快。

还有一类是依赖:“Dependency failed for My App”意思是Requires=的那个单元先挂了,跟本服务无关。systemctl --failed列出机器上所有失败单元,再对着那个单元重走一遍流程。

版本差异单独提一句。Ubuntu 22.04、24.04,Debian 12、13自带的systemd分别是249、255、252、257。Type=notify-reload要253起,RestartSteps与RestartMaxDelaySec要254起。在低版本上写这些指令不会报错,只是被静默忽略,于是同一份unit文件在这台机器上按预期重启、在另一台上永远只等RestartSec,排查时容易钻牛角尖。systemd-analyze security能顺手看看沙箱开得够不够,systemd-analyze critical-chain则用来查开机启动慢是谁拖的。

数据截至2026年10月。