有效排查常见的 Systemd 服务故障
使用 systemctl、journalctl、退出码、单元检查和实用修复步骤诊断常见的 systemd 服务故障。
有效排查常见的 Systemd 服务故障
大多数 systemd 服务故障并不神秘,只要你区分三个问题:systemd 是否读取了单元文件、是否成功执行了命令、以及应用程序在启动后是否保持健康。这些是不同的故障点,它们会留下不同的线索。
我经常看到的一个错误是直接跳到编辑单元文件。首先检查状态和日志。一个失败的服务通常会告诉你它遇到了缺失的可执行文件、错误的用户、权限问题、依赖顺序问题还是应用程序崩溃。确切的措辞很重要。
基本诊断工具包
有效的故障排查依赖于两个主要的 systemd 工具,它们能提供关于服务状态和操作日志的即时反馈。
1. 检查服务状态
systemctl status 命令提供单元状态的即时快照,包括其当前状态、最近的日志以及关键元数据,如进程 ID (PID) 和退出码。
$ systemctl status myapp.service
需要查找的关键信息:
Load:确认单元文件被正确读取。loaded表示正常。如果显示not found,则你的服务文件位置错误或拼写错误。Active:这是核心状态。如果显示failed,则表示服务尝试启动但意外退出。Exit Code:这个数字代码通常与Active: failed一起显示,至关重要。它指示进程终止的原因(例如,0 表示正常退出,1 或 2 表示一般应用程序错误,203 表示执行路径错误)。- 最近的日志: Systemd 通常会包含服务日志输出的最后几行,这可能会立即揭示错误。
2. 使用 Journalctl 深入查看日志
虽然 systemctl status 提供了摘要,但 journalctl 提供了服务执行历史的完整上下文,包括标准输出和标准错误流。
使用以下命令查看特定失败服务的日志,使用 -x 标志获取解释,使用 -e 标志跳转到末尾(最近的条目):
$ journalctl -xeu myapp.service
提示: 如果故障发生在几小时或几天前,请使用时间过滤选项,例如
journalctl -u myapp.service --since "2 hours ago"。
常见故障的逐步诊断
Systemd 故障通常属于几个可预测的类别。通过检查状态和日志,你可以快速对问题进行分类并应用适当的解决方案。
故障类型 1:执行错误(退出码 203)
退出码 203/EXEC 表示 systemd 无法执行 ExecStart 指令中指定的文件。这是最常见的配置错误之一。
原因和解决方案:
路径不正确: 可执行文件的路径错误或不是绝对路径。
- 解决方案: 始终在
ExecStart中使用完整的绝对路径。确保可执行文件确实存在于该确切位置。
# 错误 ExecStart=myapp # 正确 ExecStart=/usr/local/bin/myapp- 解决方案: 始终在
缺少权限: 文件缺少运行服务的用户的执行权限。
- 解决方案: 检查并应用执行权限:
chmod +x /path/to/executable。
- 解决方案: 检查并应用执行权限:
缺少解释器(Shebang): 如果
ExecStart指向一个脚本(例如 Python 或 Bash),则 shebang 行(#!/usr/bin/env python)可能缺失或不正确,从而阻止执行。- 解决方案: 验证脚本具有有效的 shebang 行。
故障类型 2:应用程序崩溃(退出码 1 或 2)
如果服务成功启动(systemd 找到了可执行文件)但随后立即进入 failed 状态并显示通用应用程序错误代码(通常为 1 或 2),则问题出在应用程序逻辑或环境中。
原因和解决方案:
配置文件错误: 应用程序无法读取其所需的配置文件,或者文件包含无效语法。
- 解决方案: 仔细检查
journalctl输出。应用程序通常会打印关于配置文件路径或语法的特定错误消息。如果配置文件是相对路径,请使用WorkingDirectory=指令。
- 解决方案: 仔细检查
资源争用/访问被拒绝: 由于权限限制,应用程序无法打开必要的端口、访问数据库或写入日志文件。
- 解决方案: 验证服务文件中的
User=指令,并确保该用户对所有必要的资源和目录具有读写访问权限。
- 解决方案: 验证服务文件中的
故障类型 3:依赖项故障
服务可能因为在其所需依赖项(如数据库、网络接口或挂载的文件系统)准备就绪之前启动而失败。
原因和解决方案:
网络未就绪: 需要网络连接的服务(例如 Web 服务器、代理)如果在网络堆栈初始化之前启动,通常会失败。
- 解决方案: 如果服务在启动时需要地址或路由,请添加
network-online.target排序,并确保你的网络管理器的等待在线服务已启用:
[Unit] Description=我的 Web 服务 After=network-online.target Wants=network-online.target- 解决方案: 如果服务在启动时需要地址或路由,请添加
文件系统未挂载: 服务尝试访问尚未挂载的卷上的文件(对于辅助存储或网络挂载尤其关键)。
- 解决方案: 使用
RequiresMountsFor=明确告诉 systemd 在启动前必须提供哪个路径。
[Unit] RequiresMountsFor=/mnt/data/storage- 解决方案: 使用
故障类型 4:用户和环境问题(退出码 217)
退出码 217/USER 通常表示与用户或组指令相关的问题,或者环境变量不可用。
原因和解决方案:
无效的用户/组:
User=或Group=指令中指定的用户在系统上不存在。- 解决方案: 通过
id <username>验证用户名是否存在。
- 解决方案: 通过
缺少环境变量: Systemd 服务在干净的环境中运行,这意味着 shell 变量(如
PATH或自定义 API 密钥)不会被继承。- 解决方案: 直接在服务文件中或通过环境文件定义必要的变量。
[Service] # 直接定义 Environment="API_KEY=ABCDEFG" # 使用外部文件(例如 /etc/sysconfig/myapp) EnvironmentFile=/etc/sysconfig/myapp
故障排查工作流和最佳实践
修改服务文件时,始终遵循以下三步循环,以确保你的更改被正确应用和测试。
1. 验证配置语法
在尝试启动服务之前,使用 systemd-analyze verify 检查服务单元文件。这可以捕获简单的语法错误。
$ systemd-analyze verify /etc/systemd/system/myapp.service
2. 重新加载守护进程
Systemd 会缓存配置文件。对单元文件进行任何更改后,你必须告诉 systemd 重新加载其配置。
$ systemctl daemon-reload
3. 重启并检查状态
尝试重启服务并立即检查其状态和日志。
$ systemctl restart myapp.service
$ systemctl status myapp.service
处理即时重启和超时
如果你的服务进入 restarting 循环或立即失败而没有明显的日志消息,请考虑调整 [Service] 部分中的这些指令:
| 指令 | 目的 | 最佳实践 |
|---|---|---|
Type= |
systemd 如何管理进程(例如 simple、forking)。 |
除非应用程序明确守护进程化,否则使用 simple。 |
TimeoutStartSec= |
systemd 等待主进程发出成功信号的时间。 | 如果应用程序启动时间较长(例如大型数据库初始化),请增加此值。 |
Restart= |
定义何时应自动重启服务(例如 always、on-failure)。 |
对于生产应用程序,使用 on-failure 以防止因重复配置错误而导致无限重启循环。 |
更仔细地读取故障状态
failed 不是唯一的不良状态。单元在正常退出后可能是 inactive (dead),这对于 Type=oneshot 作业来说是正常的,但对于你期望持续运行的守护进程来说则可疑。单元可能是 activating 直到 TimeoutStartSec= 过期。单元可能是 active (exited),当命令完成且 systemd 认为这是可接受的。在更改重启策略之前,请确保服务类型与程序匹配。
对于正常的前台进程,从以下开始:
[Service]
Type=simple
ExecStart=/usr/local/bin/myapp
对于运行一次然后退出的脚本:
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/rotate-reports
对于将自己 fork 到后台的旧守护进程,可能需要 Type=forking,但不要习惯性地使用它。许多现代应用程序在 systemd 下运行时已经保持在前台。如果你告诉 systemd 期望 fork 而进程没有按照 systemd 期望的方式 fork,你可能会得到误导性的启动失败。
在压力下有效的分诊检查清单
当服务宕机且人们在等待时,使用固定的顺序:
systemctl status myapp.service --no-pager
journalctl -u myapp.service -b --no-pager
systemctl cat myapp.service
systemctl show myapp.service -p FragmentPath -p User -p Group -p WorkingDirectory -p ExecStart
查找第一个真正的错误,而不是最后一行。最后的日志条目可能只说明 systemd 将单元标记为失败。有用的行通常在其上方:Permission denied、No such file or directory、Address already in use、Failed at step USER 或特定于应用程序的异常。
如果服务最近被编辑过,请检查语法和重新加载状态:
sudo systemd-analyze verify /etc/systemd/system/myapp.service
sudo systemctl daemon-reload
如果 systemctl status 说单元文件在磁盘上已更改,则 systemd 正在警告你管理器尚未重新加载新定义。在 daemon-reload 之前重启服务可能会继续使用过时的设置。
看起来不像权限问题的权限问题
服务可能从你的 shell 完美运行,但在 systemd 下失败,因为它不是以你的身份运行。检查 User=、Group=、WorkingDirectory= 以及任何强化选项,例如 ProtectSystem=、ReadWritePaths=、PrivateTmp= 或 NoNewPrivileges=。
例如:
[Service]
User=webapp
WorkingDirectory=/srv/webapp
ExecStart=/srv/webapp/bin/server
ReadWritePaths=/srv/webapp/var
ProtectSystem=strict
使用 ProtectSystem=strict,大部分文件系统对服务是只读的。这是一个很好的强化设置,但意味着应用程序只能写入你明确允许的路径。如果日志显示应用程序无法创建 PID 文件、缓存文件、SQLite 数据库或上传目录,则单元的沙箱可能是原因。
同时检查父目录权限。可执行文件可能是模式 755,但如果 /srv/webapp 对服务用户不可搜索,systemd 仍然无法执行它。使用:
namei -l /srv/webapp/bin/server
sudo -u webapp /srv/webapp/bin/server --check-config
以服务用户身份运行安全的配置检查可以捕获许多问题,而无需启动完整的守护进程。
重启循环和速率限制
Restart=on-failure 很有用,但它可能会在大量重复启动中隐藏原始错误。Systemd 还应用启动速率限制。当服务在短时间内失败太多次时,你可能会看到 start-limit-hit。
有用的命令:
systemctl status myapp.service
systemctl reset-failed myapp.service
sudo systemctl start myapp.service
reset-failed 不能解决原因。它只清除 systemd 的失败状态和速率限制内存,以便你可以在进行更改后再次测试。如果你一直需要它,请放慢速度并修复日志中的第一个故障。
调试持久性问题
如果标准日志没有揭示问题,应用程序可能正在重定向其输出。
- 检查
StandardOutput和StandardError: 默认情况下,这些被定向到日志。如果它们被设置为/dev/null或文件,你必须直接检查这些位置以获取错误消息。 - 临时详细输出: 如果可能,临时配置应用程序(或其
ExecStart中的命令行参数)以最大详细程度运行(例如--debug或-v),以便在失败时生成更详细的日志输出。
合理的停止点
一旦服务启动,再检查一件事:它是否执行实际工作。systemctl status 只能从 systemd 的角度告诉你进程状态。一个 Web 服务可能在返回 500 错误时处于活动状态。一个工作进程可能在每个作业都失败时处于活动状态。在修复单元级问题后,运行应用程序自身的健康检查,查看其应用程序日志,并确认它与之通信的依赖项是可访问的。
对于大多数事件,有用的路径很短:systemctl status,然后 journalctl -u,然后使用 systemctl cat 检查单元,然后以配置的服务用户身份测试命令。这让你接近证据,远离随机的单元文件更改。
在服务运行手册或部署说明中记下最终原因,趁它还在脑海中。"修复了 systemd" 以后没有用。"服务因 203/EXEC 失败,因为部署创建了 /opt/app/current/bin/server 而没有执行权限" 是有用的。下一次事件通常与上一次类似。