Systemd 服务故障排查:逐步指南

通过状态检查、日志分析、单元文件审查、依赖修复和环境调试来诊断 systemd 服务故障。

Systemd 服务故障排查:逐步指南

当您放慢脚步并遵循证据时,Systemd 服务故障更容易调试。一个失败的单元通常会留下三个有用的线索:systemd 记录的状态、它尝试运行的命令以及 systemd 或应用程序写入的日志。如果您按顺序阅读这些内容,就可以避免常见的陷阱——在您知道问题是单元、应用程序、依赖项还是主机之前就编辑单元文件。

下面的示例使用虚构的 mywebapp.service,但相同的工作流程适用于数据库助手、队列消费者、备份作业、导出器和内部守护进程。

第一道防线: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 等标志:

  • 显示自特定时间以来的日志:
    journalctl -u mywebapp.service --since "10 minutes ago"
    journalctl -u mywebapp.service --since "2023-10-26 10:00:00"
    
  • 仅显示错误级别或更高级别的消息:
    journalctl -u mywebapp.service -p err
    
  • -xe 结合使用以获取扩展说明和详细输出:
    journalctl -u mywebapp.service -xe --since "5 minutes ago"
    
    -x 可以为某些 systemd 消息添加解释性文本。将这些解释视为提示,而不是替代特定于单元的日志。

理解日志消息

查找诸如 ErrorFailedWarning 或指示出错的特定于应用程序的消息等关键字。注意时间戳以了解导致失败的事件顺序。

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

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

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

检索单元文件

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

systemctl cat mywebapp.service

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

要检查的关键指令

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

  • ExecStart:这是 systemd 执行以启动服务的命令。验证路径是否正确,命令本身是否可执行,并且在手动调用时(例如,作为指定的 User)是否成功运行。
    ExecStart=/usr/local/bin/mywebapp-start.sh
    
  • Type:定义进程启动类型。常见类型包括:
    • simple(默认):ExecStart 是主进程。
    • forkingExecStart 派生一个子进程,父进程退出。Systemd 等待父进程退出。
    • oneshotExecStart 运行并退出;只要命令在运行,systemd 就认为服务处于活动状态。
    • notify:服务在就绪时向 systemd 发送通知。
    • 错误的 Type 可能导致 systemd 认为服务失败而实际上它已启动,反之亦然。
  • User / Group:服务将运行的用户和组。权限问题通常源于服务尝试访问其在此用户下没有权限的文件或资源。
    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.servicenetwork.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= 指令: 确保它们正确反映了所需的启动顺序和严格性。有时,服务需要等待特定端口打开,而不仅仅是等待另一个单元的启动作业完成。对于狭窄的检查,ExecStartPre= 可以提供帮助。对于网络守护进程,套接字激活或应用程序级别的重试逻辑通常更可靠。

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

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

Systemd 的干净环境

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

症状: ExecStart=/usr/local/bin/myscript.sh 失败,并显示 python: command not foundnode: command not found、缺少库错误,或应用程序消息说所需设置为空。

修复: 使服务环境显式化。

[Service]
WorkingDirectory=/opt/mywebapp
Environment="APP_ENV=production"
Environment="PATH=/opt/mywebapp/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/opt/mywebapp/venv/bin/gunicorn app:app

对于许多变量,使用环境文件:

[Service]
EnvironmentFile=/etc/mywebapp/mywebapp.env
ExecStart=/opt/mywebapp/bin/server

保持该文件简单。EnvironmentFile= 不是 Bash 脚本。使用 KEY=value 行,而不是 export KEY=value、命令替换或 shell 条件语句。如果文件包含机密,还要设置限制性权限:

sudo chown root:mywebapp /etc/mywebapp/mywebapp.env
sudo chmod 0640 /etc/mywebapp/mywebapp.env

权限:以服务用户身份重现失败

权限问题很常见,因为手动测试通常以 root 或您的登录用户身份进行,而单元以专用服务帐户运行。

检查配置的用户:

systemctl show mywebapp.service -p User -p Group

然后以该用户身份运行相同的命令:

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

如果应用程序需要工作目录,请包含它:

sudo -u mywebappuser bash -lc 'cd /opt/mywebapp && /usr/local/bin/mywebapp-start.sh'

不要只看可执行文件。服务用户可能需要读取 /etc/mywebapp/config.yml 的权限、写入 /var/lib/mywebapp 的权限、每个父目录的执行权限,或者在 /run/mywebapp 下创建 Unix 套接字的权限。快速检查可以节省大量猜测:

sudo -u mywebappuser test -r /etc/mywebapp/config.yml
sudo -u mywebappuser test -w /var/lib/mywebapp
namei -l /var/lib/mywebapp/uploads

如果服务仅在绑定到低端口(如 80 或 443)时失败,请不要立即以 root 身份运行它。根据服务,反向代理、套接字激活或定向能力可能更安全。

启动限制和重启循环

反复崩溃的服务可能会停止并显示消息 start request repeated too quickly。这意味着 systemd 的速率限制已触发。原始失败发生在更早的时候,所以不要只关注速率限制消息。

使用:

journalctl -u mywebapp.service --since "30 minutes ago"
systemctl show mywebapp.service -p NRestarts -p Restart -p StartLimitBurst -p StartLimitIntervalUSec

修复根本原因后,清除失败状态:

sudo systemctl reset-failed mywebapp.service
sudo systemctl start mywebapp.service

小心使用 Restart=always。它对弹性守护进程很有用,但在调试期间,它可能会淹没日志并隐藏第一个清晰的错误。您可以暂时停止单元,查看日志,并在更改一件事后手动启动它。

在重新加载之前验证单元

在编辑单元文件后重新启动服务之前,验证文件并重新加载 systemd:

sudo systemd-analyze verify /etc/systemd/system/mywebapp.service
sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

如果服务有插入式覆盖,请检查合并后的版本:

systemctl cat mywebapp.service
systemctl show mywebapp.service -p FragmentPath -p DropInPaths -p ExecStart

这可以捕获尴尬的情况:您编辑了 /usr/lib/systemd/system 下的文件,但 /etc/systemd/system/mywebapp.service.d/override.conf 下的插入式文件仍然更改了 ExecStart;或者您修复了复制的单元文件,但该文件不是 systemd 加载的那个。

实用的操作顺序

当生产服务宕机时,使用一个简短、可重复的循环:

  1. 运行 systemctl status mywebapp.service --no-pager
  2. 读取 journalctl -u mywebapp.service --since "15 minutes ago"
  3. 检查 systemctl cat mywebapp.service
  4. 检查命令、用户、工作目录、环境和依赖项。
  5. 以服务用户身份重现命令。
  6. 进行一次更改。
  7. 如果单元已更改,运行 systemctl daemon-reload
  8. 重新启动并再次检查日志。

这个顺序使调查保持脚踏实地。如果日志显示 Permission denied,修复权限。如果显示 No such file or directory,从 systemd 的角度检查路径。如果显示 Dependency failed,首先调试依赖项。如果显示进程以状态 0/SUCCESS 退出但服务失败,检查 Type= 以及应用程序是否守护化或立即退出。

目标不是记住每个 systemd 指令。而是将失败消息与产生它的层匹配起来。