起不来的 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/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 文件”没反应” | 守护进程没重载 | 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 什么都没有,按顺序查三件事:
- unit 里的
StandardOutput=和StandardError=——必须是journal(默认)或journal+console。可能有人把它们设成了null。 - 应用写文件而不写 stdout。systemd 只捕获 stdout/stderr;写日志文件的应用完全绕开 journal。要么让应用改写 stdout,要么去读那个文件。
- 存储上限把旧行挤掉了:
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 秒检查清单
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 文件,顺序别反。
— mrsaynothing
Get the next one by email
One email per post. No spam, no algorithms.
what is this?喜欢这些文章?我的本职工作就是这样的工程。 雇用我