■ 空间 · 散文 lane

■ 先给答案

systemctl status + journalctl -u <unit> -n 50 --no-pager — 五种原因之一已在日志里点名.

203/EXEC = 路径、shebang 或执行位 — systemd 启动 ExecStart= 时不经 shell。

只在开机时失败? 启动次序竞态 — 加上 After=/Wants= 和一个 ExecStartPre= 守卫。

两条命令

systemctl status myapp.service
journalctl -u myapp.service -n 50 --no-pager

status 给你状态 — inactive (dead), failed (exit-code), activating (auto-restart) — 以及最后几行日志。journal 讲完整的故事:stdout、stderr,和 systemd 自己对 unit 的抱怨。以前也失败过、想看当时的 那份 记录?加 -b 管当前启动,或 --since today。还有一对值得知道:

systemctl list-units --failed     # every red unit on the box
systemctl reset-failed myapp.service   # clear the failed state after the fix

verify: systemctl list-units --failed → 列出 0 个已加载 unit

五种原因

日志点名症状之后,原因几乎总是这几个之一——先查最可能的:

日志说可能的原因修法
status=203/EXEC错误的 ExecStart=路径或解释器缺失绝对路径,chmod +x,检查 shebang
status=203/EXEC 出现在脚本上CRLF 行尾或坏的 shebangdos2unix script.sh,修好第一行
status=1/FAILURE,应用日志全无工作目录或环境变量缺失设置 WorkingDirectory=,加上 Environment=
Permission denied用户读不了文件或绑不了端口修正归属;1024 以下端口需要 AmbientCapabilities=CAP_NET_BIND_SERVICE 或 root
Unit is masked有人运行过 systemctl masksystemctl unmask myapp.service
unit 文件改了「没反应」daemon 未重载systemctl daemon-reload

203/EXEC 一族

吃掉整个下午的那一种。 systemd 不经过你的 shell 去启动 ExecStart= — 没有 ~ 的展开,没有 PATH 查找,没有 cd。看着错误的行变对:

ExecStart=~/app/run.shExecStart=/opt/app/run.sh没有 shell → 不展开波浪号,要求绝对路径
ExecStart=run.shExecStart=/opt/app/run.sh不按 PATH 查找 — 相对路径先死
(脚本不可执行,CRLF 行尾)chmod +x run.sh && dos2unix run.sh脚本需要执行位和真 shebang:#!/bin/bash 或 #!/usr/bin/env bash

脚本在你终端里跑得好好的、到 systemd 底下却报 203,几乎总是这四个之一:不可执行、CRLF 行尾、shebang 指向空处,或相对路径。

手动能跑,开机就死

开机失败是次序问题,不是语法。 经典竞态:服务在某个东西出现之前就伸手去够网络、挂载的磁盘或数据库。别指望运气,声明依赖:

[Unit]
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
ExecStartPre=/usr/bin/test -f /opt/app/config.toml

After= 排定启动次序; Wants= 让 systemd 真正把依赖拉起来。 network-online.target 只在你的发行版启用 network-wait 服务时才有效——检查 systemctl is-enabled NetworkManager-wait-online.service(或 systemd-networkd 的等价物)。 ExecStartPre= 守卫失败时大声给出可读信息,而不是一串调用栈。

verify: systemctl is-enabled NetworkManager-wait-online.service → enabled

第二种变体:开机启动、随即死亡。找你的交互环境有而开机没有的东西 — PATH 不同、virtualenv、HOME。systemd 以精简过的环境启动 unit;你的 shell 隐式供给的一切都要明写出来:

[Service]
Environment="PATH=/opt/app/venv/bin:/usr/bin"
User=appuser
WorkingDirectory=/opt/app

碰到真谜团时,以 unit 的用户身份在 shell 里跑一遍完整的 ExecStart= 命令——大多数环境差异会在前十秒内现形。

journal 里什么都没有?

  1. StandardOutput= 和 StandardError= 出现在 unit 里——必须是 journal(默认)或 journal+console。有人可能把它们调成了 null.
  2. 应用写文件而不是 stdout。systemd 只捕获 stdout/stderr;写日志文件者完全绕开 journal。把应用指向 stdout 或去读那个文件。
  3. 存储限额丢弃了旧行: journalctl --disk-usage,以及在 SystemMaxUse= 里 /etc/systemd/journald.conf,如果 journal 正被噎住。

崩溃之后重启

默认策略是 Restart=no:崩了的服务保持死亡,你从用户嘴里才得知。按服务修这一点:

[Service]
Restart=on-failure
RestartSec=5

verify: systemctl show myapp.service --property=Restart → Restart=on-failure

设置何时重启
no(默认)从不
on-failure非零退出码、信号、超时
always任何退出,包括干净退出
on-watchdog仅 watchdog 超时

把 Restart=on-failure 和启动频率守卫配对,别让崩溃循环砸机器: StartLimitIntervalSec= 和 StartLimitBurst= 放进 [Unit] 段。六十秒内五次失败应该呼叫人类,而不是空转 CPU。如果你要把它接成定时任务而非 daemon,timer 白送 journal 日志和依赖次序——先在 cron 与 timer 之间称一称。

60 秒清单

  1. systemctl status <unit> — 读状态和最后几行。
  2. journalctl -u <unit> -n 50 --no-pager — 找到真正的错误。
  3. 203/EXEC?修路径、shebang、权限。 Permission denied?修用户和文件归属。
  4. 只在开机时失败?加上 After=/Wants=network-online.target 和一个 ExecStartPre= 守卫。
  5. 改了 unit 文件? systemctl daemon-reload && systemctl restart <unit>.
  6. 加上 Restart=on-failure,让下次崩溃自己宣布,而不是藏起来。

大多数「systemd 好复杂」的时刻都能归约为 错误从头到尾都在 journal 里。先读它,再改 unit 文件。

■ 诚实的盖印 工具:systemd · journalctl · unit 文件。 首发于 2026-09-14,记录已在 2026-10-04 刷新。五种原因、203/EXEC 的机理和重启策略都是 systemd 的文档行为——上面的每个 unit 片段都是真实机器上跑着的形态,从失败后回归的 unit 里敲出来的。

五种原因到此为止 · 未取任何远程内容 · 全程本地