排查 systemd 服务故障:系统管理员实用指南
Systemd 服务是现代 Linux 系统的基石,但它们也可能出现故障。本实用指南帮助系统管理员系统性地排查并解决常见的 systemd 服务故障。学习如何有效使用 `journalctl` 进行日志分析、诊断依赖问题、解读退出码,并针对 Web 服务器、数据库等应用实施具体修复,以快速恢复服务功能。
排查 systemd 服务故障:系统管理员实用指南
失败的 systemd 服务通常并不像初看时那么神秘。有用的证据已经在机器上:单元定义、systemd 尝试运行的精确命令、退出状态以及故障发生前后的日志行。关键在于按正确的顺序读取它们,而不是重启服务十次并期望消息发生变化。
我通常从三个问题开始:systemd 是否找到了单元、进程是否启动、以及应用程序本身是否拒绝了其配置或环境?下面的命令让这种调查保持脚踏实地。
理解 Systemd 服务故障
当 systemd 服务无法启动或意外崩溃时,原因通常多种多样。这些可能包括简单的配置错误、缺少依赖项、资源限制,甚至是服务本身的 bug。Systemd 提供了强大的机制来帮助你精确定位这些故障的确切原因。
服务故障的常见原因:
- 配置错误: 服务的
.service单元文件或相关配置文件中的设置不正确。 - 缺少依赖项: 服务依赖的其他系统资源(如网络、其他服务、特定文件系统)不可用或尚未启动。
- 资源耗尽: 服务所需的内存、CPU 或磁盘 I/O 超过了系统所能提供的量。
- 权限问题: 服务进程缺乏访问所需文件、目录或网络端口所必需的权限。
- 服务中的 Bug: 应用程序本身存在导致其在启动或运行期间崩溃的 bug。
- 数据损坏: 服务使用的关键数据文件已损坏。
- 网络问题: 网络接口、DNS 或防火墙规则出现问题,阻止服务绑定端口或进行通信。
步骤 1:检查服务状态
排查任何失败服务的第一步是检查其当前状态。Systemd 的 systemctl 命令是执行此操作的主要工具。
使用 systemctl status
systemctl status <service_name>.service 命令提供了服务当前状态、最近的日志条目和进程信息的简明概述。
sudo systemctl status nginx.service
示例输出(失败的服务):
● nginx.service - A high performance web server and reverse proxy
Loaded: loaded (/lib/systemd/system/nginx.service; enabled; vendor preset: enabled)
Active: failed (result=exit-code) since Tue 2023-10-27 10:30:00 UTC; 1min ago
Docs: man:nginx(8)
Process: 1234 ExecStart=/usr/sbin/nginx -g daemon on; master_process on; (code=exited, status=1/FAILURE)
Main PID: 1234 (code=exited, status=1/FAILURE)
Oct 27 10:30:00 your-server systemd[1]: Starting A high performance web server and reverse proxy...
Oct 27 10:30:00 your-server nginx[1234]: nginx: [emerg] bind() to port 80 failed (98: Address already in use)
Oct 27 10:30:00 your-server systemd[1]: nginx.service: Main process exited, code=exited, status=1/FAILURE
Oct 27 10:30:00 your-server systemd[1]: Failed to start A high performance web server and reverse proxy.
在 systemctl status 输出中要查找的关键信息:
Active::此行指示当前状态。failed是我们感兴趣的状态。它也可能显示failed (result=exit-code)或failed (result=oom-kill)。result通常提供了线索。Process::关于 systemd 尝试运行的进程的详细信息。如果显示code=exited, status=...,这至关重要。- 日志条目:最近的日志行通常包含来自服务的直接错误消息。
步骤 2:使用 journalctl 分析日志
journalctl 命令是 systemd 用于查询和显示 systemd 日志的强大工具。对于深入了解服务失败的原因至关重要。
服务的基本 journalctl 用法
要查看特定服务的日志,请使用 -u 标志:
sudo journalctl -u <service_name>.service
要实时跟踪日志:
sudo journalctl -f -u <service_name>.service
要查看上次启动以来的日志(对于在启动期间失败的服务很有用):
sudo journalctl -b -u <service_name>.service
要查看自特定时间以来的日志:
sudo journalctl --since "2023-10-27 10:00:00" -u <service_name>.service
解读 journalctl 输出
查找应用程序或 systemd 本身报告的错误消息、堆栈跟踪或特定错误代码。来自 systemctl status 的示例输出已经显示了一个关键错误:bind() to port 80 failed (98: Address already in use)。这清楚地表明另一个进程已经在使用端口 80,阻止了 Nginx 启动。
提示: 如果服务日志非常冗长,你可以限制输出:
sudo journalctl -n 50 -u <service_name>.service # 显示最后 50 行
步骤 3:检查服务依赖项和要求
Systemd 服务通常依赖于其他服务或系统资源的可用性。如果依赖项未满足,服务将无法启动。
查看依赖项
你可以使用 systemctl cat 检查服务的依赖项,并查看 Requires=、Wants=、After=、Before= 和 PartOf= 等指令。
systemctl cat <service_name>.service
例如,绑定到特定地址的服务可能需要在网络配置完成后排序。After=network-online.target 仅控制顺序;它本身不会将该目标拉入事务中。如果服务确实需要它,你通常会看到两者:
Wants=network-online.target
After=network-online.target
谨慎使用 Requires=。它会创建更强的依赖关系,并可能在所需单元停止时停止你的服务。许多应用程序服务只需要 Wants= 加上 After=。
检查缺少的依赖项
虽然 systemctl status 通常能指示依赖项问题,但明确检查所需服务是否处于活动状态可能会有所帮助。
systemctl is-active <dependency_service_name>.service
如果所需服务被屏蔽或停止,则可能阻止你的目标服务启动。
systemctl list-dependencies <service_name>.service
此命令显示完整的依赖项树。
步骤 4:理解退出码
当服务失败时,它会以特定的退出码退出。此代码提供了关于失败性质的有价值信息。
- 退出码 0:成功。
- 退出码 1:许多程序的通用失败。具体含义取决于应用程序。
- 退出码 127:命令未找到(通常是由于
ExecStart路径不正确或缺少可执行文件)。 - 退出码 137:被
SIGKILL杀死。这通常(但不总是)与内存压力有关。 - 退出码 139:被
SIGSEGV(段错误)杀死。
从 systemctl status 输出中,我们看到 status=1/FAILURE。这是一个通用失败,前面的日志消息对于理解它为何以状态 1 失败至关重要。
识别 OOM 杀死
如果 systemctl status 显示 failed (result=oom-kill),则表示 Linux 内存不足(OOM)杀手终止了服务的进程,因为系统内存严重不足。
要确认这一点,你通常可以在 journalctl 或 dmesg 中找到相关消息:
dmesg | grep -i oom
排查 OOM 错误
- 增加系统 RAM: 如果可能。
- 减少内存使用: 优化服务或其他正在运行的进程。
- 配置交换空间: 确保有足够的交换空间可用。
- 检查服务内存限制:
MemoryMax=设置可能导致特定于服务的 OOM,即使主机仍有空闲内存。 - 审查最近的部署: 内存故障通常发生在配置更改、流量更改或版本更改之后。
步骤 5:检查 systemd 实际使用的单元文件
不要假设编辑器中的文件就是完整的单元。包、drop-in 和覆盖可以组合成最终定义:
systemctl cat <service_name>.service
systemctl show <service_name>.service -p FragmentPath -p DropInPaths
这可以捕获一个常见问题:有人编辑了 /usr/lib/systemd/system/app.service,而 /etc/systemd/system/app.service.d/override.conf 中的覆盖仍然更改了 Environment= 或 ExecStart=。
编辑单元文件或 drop-in 后,重新加载 systemd:
sudo systemctl daemon-reload
如果忘记此步骤,systemctl restart 可能会继续使用旧的单元定义。
步骤 6:常见的特定服务问题及修复
虽然上述步骤是通用的,但特定服务有其常见的故障模式。
Web 服务器(Nginx、Apache)
- 端口已被占用: 如示例所示,另一个进程可能正在监听端口 80 或 443。使用
sudo ss -tulnp | grep :80查找冲突的进程。 - 配置语法错误: 运行 Web 服务器的配置测试(例如
sudo nginx -t或sudo apachectl configtest)。 - 缺少 SSL 证书: 确保证书文件存在且可读。
数据库(MySQL、PostgreSQL)
- 数据目录权限: 确保数据库用户对其数据目录具有正确的读写权限。
- 数据文件损坏: 可能需要从备份恢复或使用数据库特定的恢复工具。
- 磁盘空间已满: 数据库可能消耗大量磁盘空间。
网络服务
- IP 地址或主机名不正确: 验证网络配置。
- 防火墙规则: 确保必要的端口已打开。
- DNS 解析问题: 检查
/etc/resolv.conf和网络连接。
步骤 7:高级排查技术
重新启用并重启服务
进行更改后,如果需要,重新加载单元,然后重启服务。除非你正在更改启动行为,否则无需每次都运行 enable。
sudo systemctl daemon-reload # 重新加载 systemd 管理器配置
sudo systemctl restart <service_name>.service
使用 systemctl --failed
此命令列出所有当前处于失败状态的单元。
systemctl --failed
检查资源限制(ulimit)
某些服务可能因达到操作系统级别的资源限制而失败。以服务运行的用户身份使用 ulimit -a 检查限制,或检查单元文件中 systemd 自身的资源控制指令。
对于 systemd 管理的服务,单元属性通常比交互式 shell 的 ulimit 更相关:
systemctl show <service_name>.service -p LimitNOFILE -p User -p Group -p MemoryMax -p TasksMax
如果应用程序显示 too many open files,请将 LimitNOFILE 与应用程序的连接数和文件使用情况进行比较。如果服务无法创建线程或子进程,请查看 TasksMax。
调试标志
许多应用程序具有调试模式或详细日志记录,可以通过 .service 文件的 ExecStart 行中的命令行参数启用。请查阅应用程序的文档。
快速示例:服务手动工作,但在启动时失败
这是最常见的 systemd 抱怨之一。开发人员手动运行命令,它工作正常。相同的命令作为服务运行却失败。通常的区别在于环境。
检查服务用户和工作目录:
systemctl show myapp.service -p User -p Group -p WorkingDirectory
systemctl cat myapp.service
然后查找应用程序中的假设:相对路径、主目录中的文件、来自 .bashrc 的环境变量或由交互式 shell 加载的凭据。Systemd 不会为服务读取你的 shell 启动文件。如果应用程序需要 APP_ENV=production 或 DATABASE_URL=...,请将该配置放在单元中,使用 Environment=、EnvironmentFile= 或你正常的密钥管理路径。
仅启动时失败也可能是排序问题。服务可能在 DNS、网络地址或挂载的文件系统准备就绪之前启动。不要通过在应用程序中盲目 sleep 来解决此问题。在单元中表达依赖关系:
Wants=network-online.target
After=network-online.target
RequiresMountsFor=/srv/myapp
当服务需要特定路径时,RequiresMountsFor= 很有用,尤其是当该路径来自单独的磁盘或网络挂载时。它比希望一个宽泛的目标恰好先完成更清晰。
重置失败状态
服务失败后,systemd 会记住失败状态,直到它被重置或单元成功。这有助于可见性,但在你已解决问题后可能会混淆状态检查:
sudo systemctl reset-failed myapp.service
sudo systemctl restart myapp.service
systemctl status myapp.service
在捕获了所需证据后使用 reset-failed。在事件期间,失败状态和日志时间戳是有用的线索。
还有一个有助于处理嘈杂故障的小习惯:在编辑任何内容之前,检查单元是否正在重启循环。
systemctl show myapp.service -p NRestarts -p RestartUSec
如果重启次数快速增加,请在调查时停止该单元。这可以保护依赖项免受重复的错误连接影响,并保持日志的可读性。
可靠的模式是:读取 status,读取日志,使用 systemctl cat 检查有效单元,验证依赖项和路径,然后仅在你了解发生了什么变化之后才重启。这使 systemd 故障排查变得平淡无奇,而这正是你在中断期间所希望的。