返回博客

systemd 服务起不来?排查与修复

2026年9月14日

起不来的 systemd 服务几乎从来不神秘。先跑 systemctl status <unit>,再用 journalctl -u <unit> -n 50 --no-pager 读该 unit 的最后 50 行日志——这两步之间,五种原因之一通常已经被点名:路径写错、二进制缺失、权限不对、SELinux/AppArmor 拒绝,或 unit 文件语法错误。失败原因就写在日志里;下面的修法不过是对着它做模式匹配。

怎么查看 systemd 服务为什么失败?

先 status,后 journal:

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 的抱怨。

如果 unit 之前失败过,而你想要那次运行的记录,加 -b 限定本次启动,或用 --since today:

journalctl -u myapp.service -b --no-pager

完整工具箱——boots、优先级、实时跟随输出——见 journalctl 速查表。同一块肌肉记忆。

另外一对值得知道:

systemctl list-units --failed
systemctl reset-failed myapp.service

第一条列出机器上所有红的 unit。第二条在你修好之后清掉 failed 状态——纯观感,但能让状态页别再狼来了。

最常见的原因有哪些?

日志报出症状之后,病因几乎总是这五个之一:

日志显示可能原因修法
status=203/EXECExecStart= 路径不对或缺解释器绝对路径,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 masksystemctl unmask myapp.service
改了 unit 文件”没反应”守护进程没重载systemctl daemon-reload

203/EXEC 一族值得单独点名,因为它最吃掉下午。systemd 不会借你的 shell 去启动 ExecStart=。这意味着:

# 错——没有 shell,~ 永远不展开,也不做 PATH 查找
ExecStart=~/app/run.sh

# 对
ExecStart=/opt/app/run.sh

脚本本身必须可执行,并以真实的 shebang 开头(#!/bin/bash#!/usr/bin/env bash)。终端里跑得好好的、到 systemd 底下就报 203 的脚本,几乎总是这几种之一:不可执行、CRLF 行尾、shebang 指向不存在的东西,或者相对路径。

为什么手动能起,开机却起不来?

经典的顺序 bug。如果日志显示失败紧跟在启动之后,但你 systemctl start 手动一跑就正常,你的服务在抢跑——它伸手去够网络、挂载盘或数据库,而那东西还不存在。

修法是声明依赖,而不是许愿:

[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 只有在你的发行版启用了网络等待服务时才有效,查一下 systemctl is-enabled NetworkManager-wait-online.service(或 systemd-networkd 的对应物)。ExecStartPre= 这个守卫便宜又诚实:用一条可读的消息大声失败,好过一串堆栈。

第二种变体:开机确实起来了,但立刻死掉。找那些你的交互环境有、boot 时没有的东西——PATH 差异、virtualenv、HOME。需要什么就显式设什么:

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

为什么服务不往 journal 里写日志?

如果 journalctl -u 什么都没有,按顺序查三件事:

  1. unit 里的 StandardOutput=StandardError=——必须是 journal(默认)或 journal+console。可能有人把它们设成了 null
  2. 应用写文件而不写 stdout。systemd 只捕获 stdout/stderr;写日志文件的应用完全绕开 journal。要么让应用改写 stdout,要么去读那个文件。
  3. 存储上限把旧行挤掉了:journalctl --disk-usage,journal 噎住时看 /etc/systemd/journald.conf 里的 SystemMaxUse=

要调试启动本身,没什么比往 boot 时的上下文里丢一个 shell 更直接:

[Service]
ExecStart=/bin/bash -c 'exec /opt/app/bin/server 2>&1'

或者,对真正的悬案,以 unit 的用户身份在 shell 里原样跑一遍 ExecStart= 那条命令——大多数环境差异十秒内现形。

崩溃之后怎么让它自动重启?

默认策略是 Restart=no:崩了的服务躺在原地,你从用户嘴里才得知消息。逐服务修掉它:

[Service]
Restart=on-failure
RestartSec=5
配置何时重启
no(默认)从不
on-failure非零退出、信号、超时
always任何退出,包括正常退出
on-watchdog仅看门狗超时

Restart=on-failure 要配一个启动速率护栏,免得崩溃循环捶机器:[Unit] 段里的 StartLimitIntervalSec=StartLimitBurst=。六十秒内失败五次应该叫人来看,而不是空烧 CPU。

如果你要搭的其实是定时任务而不是守护进程,先掂量一下 cron 还是 systemd timer——timer 白送 journal 日志和依赖排序,而这正是本文一直在伸手要的东西。

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 文件,顺序别反。

— mrsaynothing

Get the next one by email

One email per post. No spam, no algorithms.

self-hosted · no third parties · one-click unsubscribe

what is this?

GGUF 量化:该选哪一档?

喜欢这些文章?我的本职工作就是这样的工程。 雇用我