■ 空间 · 散文 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 行尾或坏的 shebang | dos2unix script.sh,修好第一行 |
status=1/FAILURE,应用日志全无 | 工作目录或环境变量缺失 | 设置 WorkingDirectory=,加上 Environment= |
Permission denied | 用户读不了文件或绑不了端口 | 修正归属;1024 以下端口需要 AmbientCapabilities=CAP_NET_BIND_SERVICE 或 root |
Unit is masked | 有人运行过 systemctl mask | systemctl unmask myapp.service |
| unit 文件改了「没反应」 | daemon 未重载 | systemctl daemon-reload |
203/EXEC 一族
吃掉整个下午的那一种。 systemd 不经过你的 shell 去启动 ExecStart= — 没有 ~ 的展开,没有 PATH 查找,没有 cd。看着错误的行变对:
脚本在你终端里跑得好好的、到 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 里什么都没有?
StandardOutput=和StandardError=出现在 unit 里——必须是journal(默认)或journal+console。有人可能把它们调成了null.- 应用写文件而不是 stdout。systemd 只捕获 stdout/stderr;写日志文件者完全绕开 journal。把应用指向 stdout 或去读那个文件。
- 存储限额丢弃了旧行:
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 秒清单
systemctl status <unit>— 读状态和最后几行。journalctl -u <unit> -n 50 --no-pager— 找到真正的错误。203/EXEC?修路径、shebang、权限。Permission denied?修用户和文件归属。- 只在开机时失败?加上
After=/Wants=network-online.target和一个ExecStartPre=守卫。 - 改了 unit 文件?
systemctl daemon-reload && systemctl restart <unit>. - 加上
Restart=on-failure,让下次崩溃自己宣布,而不是藏起来。
大多数「systemd 好复杂」的时刻都能归约为 错误从头到尾都在 journal 里。先读它,再改 unit 文件。
五种原因到此为止 · 未取任何远程内容 · 全程本地