Systemd 服务故障排除:分步指南

遇到 systemd 服务故障?本综合指南提供了一种分步方法来诊断和解决常见的启动问题。学习如何利用 `systemctl status` 和 `journalctl` 进行日志分析,仔细检查单元文件是否存在配置错误,识别并修复依赖关系问题,以及解决环境变量障碍。通过实际示例和高级技巧,您将有信心快速有效地使您的 Linux 服务重新联机。

38 浏览量

故障排除 Systemd 服务失败:分步指南

Systemd 已成为大多数现代 Linux 发行版的事实上的系统和服务管理器,在管理服务、守护进程和进程方面发挥着至关重要的作用。尽管 systemd 功能强大且高效,但由 systemd 管理的服务有时可能会启动失败,从而导致应用程序停机或系统不稳定。诊断这些故障需要系统化的方法,并利用 systemd 强大的日志记录和自省功能。

本指南提供了一种全面、分步的方法来排除 systemd 服务启动失败的常见问题。我们将涵盖从初步状态检查、深入日志分析到检查单元文件和解决复杂的依赖关系问题的所有内容。阅读本文后,您将掌握实用的知识和工具,可以高效地诊断和解决大多数 systemd 服务失败问题,确保您的应用程序和服务顺利运行。

第一道防线:systemctl status

当服务启动失败时,您应该运行的第一个命令是 systemctl status <service_name>。此命令提供服务当前状态的快照,包括它是否处于活动状态、是否已加载,以及最关键的——其最近日志的摘要。这通常足以快速识别问题。

假设您的 Web 应用程序服务 mywebapp.service 未启动:

systemctl status mywebapp.service

示例输出解读:

● mywebapp.service - My Web Application
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: disabled)
     Active: failed (Result: exit-code) since Mon 2023-10-26 10:30:05 UTC; 10s ago
    Process: 12345 ExecStart=/usr/local/bin/mywebapp-start.sh (code=exited, status=1/FAILURE)
   Main PID: 12345 (code=exited, status=1/FAILURE)
        CPU: 10ms

Oct 26 10:30:05 hostname systemd[1]: Started My Web Application.
Oct 26 10:30:05 hostname mywebapp-start.sh[12345]: Error: Port 8080 already in use
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Main process exited, code=exited, status=1/FAILURE
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Failed with result 'exit-code'.

从输出中,我们可以立即看到:
* 服务 mywebapp.service 处于 failed 状态。
* 它以 Result: exit-code 失败,这意味着 ExecStart 命令以非零状态退出。
* Process 行显示命令 mywebapp-start.shstatus=1/FAILURE 失败。
* 最关键的是,日志行显示:Error: Port 8080 already in use。这是问题的明确指示。

此命令是您的第一个诊断工具,通常会直接指向原因或缩小查找范围。

使用 journalctl 深入了解

虽然 systemctl status 提供了一个快速摘要,但 journalctl 是您进行详细日志记录的首选命令。它查询 systemd 日志,该日志收集系统所有部分(包括服务)的日志。

基本日志审查

要查看特定服务的全部日志(包括历史记录):

journalctl -u mywebapp.service

这将显示与 mywebapp.service 相关的所有日志条目。如果服务反复失败,您将看到每次失败尝试的条目。

过滤和基于时间的查询

要缩小结果范围,尤其是在最近一次失败后,您可以使用 --since--priority 等标志:

  • 显示自特定时间以来的日志:
    bash journalctl -u mywebapp.service --since "10 minutes ago" journalctl -u mywebapp.service --since "2023-10-26 10:00:00"
  • 仅显示错误级别或更高级别的消息:
    bash journalctl -u mywebapp.service -p err
  • 结合 -xe 进行扩展解释和详细输出:
    bash journalctl -u mywebapp.service -xe --since "5 minutes ago"
    这非常有用,因为 journalctl -xe 提供了额外的上下文,包括对某些日志消息的解释以及可用的堆栈跟踪。

理解日志消息

查找诸如 ErrorFailedWarning 或指示问题所在的应用特定消息之类的关键字。注意时间戳以了解导致故障的事件顺序。

提示: 如果您的服务的 ExecStart 脚本输出到标准输出或标准错误,这些消息通常会被 journalctl 捕获。请确保您的脚本记录描述性的错误消息。

检查单元文件:服务的蓝图

每个 systemd 服务都由一个单元文件(例如 mywebapp.service)定义。此文件中的配置错误是启动失败的常见原因。您需要了解服务试图做什么。

检索单元文件

要查看您服务的活动单元文件:

systemctl cat mywebapp.service

此命令显示 systemd 正在使用的确切单元文件,包括任何覆盖。

需要检查的关键指令

专注于 [Service] 部分以解决执行相关问题,并关注 [Unit] 部分以解决依赖关系。

  • ExecStart:这是 systemd 执行以启动服务的命令。验证路径是否正确,并且命令本身是否可执行,以及在以指定的 User 手动调用时是否成功运行。
    ini ExecStart=/usr/local/bin/mywebapp-start.sh
  • Type:定义进程启动类型。常见类型包括:
    • simple(默认):ExecStart 是主进程。
    • forkingExecStart 分叉一个子进程,父进程退出。Systemd 等待父进程退出。
    • oneshotExecStart 运行并退出;只要命令在运行,systemd 就认为服务处于活动状态。
    • notify:服务准备就绪时向 systemd 发送通知。
    • 不正确的 Type 可能导致 systemd 认为服务已启动但实际上失败,反之亦然。
  • User / Group:服务将在其下运行的用户和组。权限问题通常源于服务尝试访问在此用户下没有权限的文件或资源。
    ini User=mywebappuser Group=mywebappgroup
  • WorkingDirectory:服务将从中执行的目录。ExecStart 或其他命令中的相对路径取决于此项。
  • Restart:定义何时应重新启动服务。如果设置为 on-failurealways,失败的服务可能会不断重新启动,从而更难捕获初始失败。
  • TimeoutStartSec / TimeoutStopSec:systemd 等待服务启动或停止的时间。如果服务的初始化时间超过 TimeoutStartSec,systemd 将会终止它并报告失败。

常见的单元文件问题

  • 不正确的路径ExecStart 或其他文件路径中的拼写错误。
  • 缺少 Environment 变量:服务通常需要特定的环境变量(例如 PATH),这些变量可能在 systemd 的干净环境中不存在(见下文)。
  • 权限:指定的 User 对脚本没有执行权限,或对必要的数据文件没有读/写权限。
  • 语法错误:单元文件本身的简单拼写错误。

要手动测试 ExecStart

切换到服务的用户并尝试直接运行命令:

sudo -u mywebappuser /usr/local/bin/mywebapp-start.sh

这通常会在终端中直接重现 journalctl 中看到的错误,从而更轻松地进行调试。

依赖管理:当服务无法单独启动时

服务通常依赖于其他服务或系统组件在它们自身启动之前处于活动状态。Systemd 使用 WantsRequiresAfterBefore 指令来管理这些依赖关系。

识别依赖关系

使用 systemctl list-dependencies <service_name> 查看服务明确需要或想要运行的内容。

systemctl list-dependencies mywebapp.service

[Unit] 部分的常见指令:

  • After=:指定此服务应列出的单元启动之后启动。如果列出的单元失败,此服务仍将尝试启动(除非也使用了 Requires=)。
  • Requires=:指定此服务需要列出的单元。如果任何必需的单元启动失败,此服务将不会启动。
  • Wants=Requires= 的弱化形式。如果某个被请求的单元失败,此服务仍将尝试启动。

示例:

[Unit]
Description=My Web Application
After=network.target mysql.service
Requires=mysql.service

在此,mywebapp.service 将在 network.targetmysql.service 启动后才启动,并且它需要 mysql.service 成功。如果 mysql.service 失败,mywebapp.service 将不会启动。

解决依赖冲突

如果服务由于依赖关系问题而失败,journalctl 通常会指明哪个依赖关系未能满足。例如,它可能会显示 Dependency failed for My Web Application,后跟关于 mysql.service 失败的详细信息。

解决步骤:
1. 检查依赖的服务: 运行 systemctl status <dependent_service>(例如 systemctl status mysql.service)和 journalctl -u <dependent_service> 来首先排除失败问题。
2. 验证 After=Requires= 指令: 确保它们正确反映了所需的启动顺序和严格性。有时,服务需要等待特定端口打开,而不仅仅是服务处于活动状态。对于复杂的情况,systemd-socket-activate 或自定义 ExecStartPre 脚本可能很有用。

环境变量和路径:隐藏的陷阱

Systemd 服务在非常干净和最小化的环境中运行。这通常会导致在用户 shell 中完美运行的命令在由 systemd 运行时失败,因为关键的环境变量(如 PATH)缺失。

Systemd 的干净环境

当 systemd 启动服务时,它不会继承启动 systemctl start 的用户的完整环境。例如,PATH 变量通常被精简,这意味着如果 pythonnode 等命令不在 /usr/bin/bin 等标准位置,它们可能找不到。

症状: ExecStart=/usr/local/bin/myscript.sh 以“