自己写的服务交给 systemd 管,一启动就给你这么一行:
Job for myapp.service failed because the control process exited with error code. See "systemctl status myapp.service" and "journalctl -xe" for details.点开 status 看,最后一行是code=exited, status=203/EXEC。你检查了脚本路径,明明存在,手动跑也正常,可 systemd 就是起不来。
另一种更气人的情况:服务能手动起来,enable也执行成功了,重启之后它压根没自启。
这两类故障占了 systemd 排错的一大半。下面按报错码对号入座。
一、先看它死在哪一步
别急着改 unit,先把真正的报错挖出来:
systemctl status myapp-l--no-pager journalctl-umyapp-b--no-pager这两条里status=203/EXEC那个数字是关键,它直接告诉你死因:
| 退出状态 | 真实含义 | 最常见的锅 |
|---|---|---|
203/EXEC | 执行不了 | 路径写错、没执行权限、脚本首行 shebang 错 |
217/USER | 用户不对 | User=指定的用户不存在 |
200/CHDIR | 工作目录不对 | WorkingDirectory=指向的目录不存在 |
status=1/FAILURE | 程序自己退了 | 程序内部报错,看程序自己的日志 |
timeout | 启动超时 | Type=forking但程序没 fork,或启动太慢 |
start request repeated too quickly | 反复重启被掐 | 程序秒退,Restart=always触发了启动频率限制 |
顺手做个语法体检,能提前抓出拼错的指令:
systemd-analyze verify /etc/systemd/system/myapp.service没输出就是语法没问题。有输出会直接指出哪一行有问题。
二、对着原因改 unit 文件
报 203/EXEC
三个原因,按这个顺序查:
# 1. ExecStart 里的路径写全了吗(systemd 不认相对路径)whichpython3 systemctlcatmyapp|grepExecStart# 2. 文件有执行权限吗ls-l/opt/myapp/start.shsudochmod+x /opt/myapp/start.sh# 3. 脚本第一行的 shebang 对吗head-1/opt/myapp/start.sh有坑:systemd 不会帮你展开~、$HOME这类变量,也认不了相对路径。ExecStart里一律写绝对路径。
有坑:脚本在 Windows 上编辑过,行尾是 CRLF,shebang 变成#!/bin/bash\r,bash 找不到,直接 203。用file start.sh能看到CRLF line terminators,修一下:
sed-i's/\r$//'/opt/myapp/start.sh程序其实起来了但 systemd 报超时
这是Type=设错了。前台运行的程序用默认的simple,会自己 fork 到后台的老程序要用forking:
[Service] Type=forking PIDFile=/var/run/myapp.pid有坑:Type=forking必须配PIDFile,否则 systemd 找不到主进程,会误判成启动失败。反过来,前台程序错设成forking,systemd 会一直等到超时。
依赖的服务还没起来就抢跑
[Unit] After=network.target postgresql.service Wants=postgresql.serviceAfter只管顺序,Wants才是真去拉起依赖。用Requires的话依赖挂了自己也跟着挂,一般服务用Wants更稳。
有坑:After=network.target只表示网络服务管理器起来了,不代表网卡已经拿到 IP。要等真正联网,用network-online.target并配合Wants=network-online.target。
程序秒退,想让它自动拉起来
[Service] Restart=on-failure RestartSec=5 StartLimitIntervalSec=0Restart=on-failure表示非正常退出才重启,always是退出就重启(包括你手动 stop 的情况,慎用)。
有坑:StartLimitIntervalSec默认 10 秒内最多启动 5 次,超了就报start request repeated too quickly并且不再尝试。程序启动慢的话把它设成 0(不限)或者调大。
需要环境变量
[Service] Environment="JAVA_HOME=/usr/lib/jvm/java-11" EnvironmentFile=/etc/myapp/env.confEnvironmentFile里按KEY=value一行一个写。
三、能跑起来之后,让它开机自启
sudosystemctl daemon-reload# 改了 unit 文件必须跑这条sudosystemctlenable--nowmyapp# 设自启 + 立刻启动systemctl is-enabled myappis-enabled的几种输出,含义不一样:
| 输出 | 状态 | 怎么办 |
|---|---|---|
enabled | 已自启 | 正常 |
disabled | 未自启 | systemctl enable myapp |
static | 不能自启,只能被别人拉 | 正常,检查是谁 Wants 它 |
masked | 被彻底屏蔽 | systemctl unmask myapp |
有坑:masked状态很多人不知道怎么来的,通常是之前有人执行过systemctl mask。它比disable更狠,连手动 start 都不行,必须unmask才解。
有坑:改了 unit 文件不跑daemon-reload,systemd 用的还是内存里的旧版本。你改半天没效果,八成卡在这一步。
四、enable 成功但重启后没起来?查这三处
1. WantedBy 跟当前 target 对不上
systemctl get-default systemctlcatmyapp|grepWantedBy服务器版默认multi-user.target,桌面版默认graphical.target。unit 里写WantedBy=graphical.target而机器跑在multi-user.target上,就不会自启。
最稳的写法是:
[Install] WantedBy=multi-user.targetmulti-user.target在桌面版上同样会被拉起。
2. 服务是 socket 激活的
有些服务没有 service 在跑,靠.socket单元守着端口,来请求才拉起:
systemctl list-units--type=socket--all|grepmyapp这种情况systemctl status myapp.service显示 inactive 是正常的,看 socket 单元就行。
3. 开机太早,依赖的东西还没就绪
启动时磁盘还没挂载完、网络还没通。加上:
[Unit] After=network-online.target remote-fs.target Wants=network-online.target想看服务是不是拖慢了开机,用这条:
systemd-analyze blame|head-20五、防复发:unit 文件交付前过一遍清单
写完 unit 别直接上生产,对着这份清单过一遍:
cat>~/checkunit.sh<<'EOF' #!/bin/bash U=$1 echo "== 语法体检 ==" systemd-analyze verify /etc/systemd/system/$U 2>&1 | head -10 echo "== ExecStart 是否绝对路径 ==" systemctl cat $U | grep -E '^ExecStart' | grep -v '^ExecStart=/' && echo "有相对路径,改掉" || echo "OK" echo "== 执行权限 ==" P=$(systemctl cat $U | grep -m1 '^ExecStart=' | sed 's/ExecStart=//' | awk '{print $1}') [ -x "$P" ] && echo "OK: $P 可执行" || echo "有问题: $P 不可执行或不存在" echo "== WorkingDirectory 是否存在 ==" W=$(systemctl cat $U | grep -m1 '^WorkingDirectory=' | cut -d= -f2) [ -z "$W" ] || { [ -d "$W" ] && echo "OK: $W" || echo "有问题: $W 不存在"; } echo "== 自启状态 ==" systemctl is-enabled $U 2>&1 EOFchmod+x ~/checkunit.sh用法:./checkunit.sh myapp.service。
还有一条经验:别直接改/usr/lib/systemd/system/里的 unit。那个目录是软件包装的,系统升级会被覆盖。要改就复制到/etc/systemd/system/再改,/etc下优先级最高。
附:unit 文件结构与指令速查
unit 三段结构:
[Unit] Description=服务说明 After=network.target # 启动顺序:在谁之后 Wants=postgresql.service # 弱依赖:会去拉起,对方挂了不影响自己 Requires=postgresql.service # 强依赖:对方挂了自己也挂 [Service] Type=simple # simple(前台) / forking(后台,需PIDFile) / oneshot / notify User=appuser WorkingDirectory=/opt/myapp Environment="KEY=value" EnvironmentFile=/etc/myapp/env.conf ExecStart=/opt/myapp/start.sh ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure # no/on-success/on-failure/on-abnormal/always RestartSec=5 StartLimitIntervalSec=0 [Install] WantedBy=multi-user.target # 服务器版与桌面版通用的自启目标常用指令:
systemctl daemon-reload 改完 unit 必跑 systemctl enable --now 服务 设自启并立刻启动 systemctl disable --now 服务 取消自启并停止 systemctl is-enabled 服务 查自启状态 systemctl mask / unmask 服务 彻底屏蔽 / 解除屏蔽 systemctl cat 服务 看最终生效的 unit 内容 systemd-analyze verify 文件 unit 语法体检 systemd-analyze blame 看谁拖慢了开机 journalctl -u 服务 -b 看本次启动的日志systemd 排错其实有固定套路:看status=后面的退出码,按码找原因,改完daemon-reload。大部分人卡住是因为只盯着Job failed那句提示,没往下看状态码。