如何有效编写和管理自定义 Systemd 单元文件
现代 Linux 发行版主要使用 systemd 作为其初始化系统和服务管理器。对于任何需要可靠部署和管理应用程序的 Linux 系统管理员或开发人员来说,理解 systemd 至关重要。虽然许多应用程序都附带预制的 systemd 单元文件,但编写自定义单元文件的能力可以让你标准化自己的应用程序、脚本或任何自定义进程的启动、关闭和一般生命周期管理。
本文将指导你完成创建、配置和管理自定义 systemd .service 单元文件的过程。我们将探讨定义应用程序如何运行、建立依赖关系以及确保健壮运行的基本指令。完成后,你将能够将自定义服务无缝集成到 Linux 操作系统中,确保它们在启动时自动启动,在失败时重启,并使用 systemctl 轻松管理。
掌握自定义 systemd 单元文件可以让你对服务进行精细控制,提高系统稳定性,并简化管理任务。让我们深入了解管理应用程序所需的关键组件和实用步骤。
理解 Systemd 单元文件
Systemd 管理各种系统资源,称为单元,这些单元由配置文件定义。这些单元包括服务 (.service)、挂载点 (.mount)、设备 (.device)、套接字 (.socket) 等。对于管理应用程序和后台进程,.service 单元类型是最常见和最相关的。
Systemd 单元文件是纯文本文件,通常存储在特定的目录中。主要位置(按优先级顺序)是:
/etc/systemd/system/:这是自定义单元文件和覆盖的推荐位置,因为它们优先于系统默认设置,并且在系统更新后仍然存在。/run/systemd/system/:用于运行时生成的单元文件。/usr/lib/systemd/system/:包含已安装软件包提供的单元文件。请勿直接修改此目录中的文件。
将自定义单元文件放在 /etc/systemd/system/ 中,可以确保它们被 systemd 正确识别和管理。
.service 单元文件的结构
Systemd .service 单元文件分为几个部分,每个部分由 [SectionName] 标识,其中包含各种指令(键值对)。服务单元的三个主要部分是 [Unit]、[Service] 和 [Install]。
让我们分解你将使用的最关键的指令:
[Unit] 部分
此部分包含有关单元、其描述和依赖关系的通用选项。
Description:一个人类可读的字符串,描述服务。这会出现在systemctl status的输出中。
ini Description=我的自定义 Python Web 应用程序Documentation:一个指向服务文档的 URL(可选)。
ini Documentation=https://example.com/docs/my-appAfter:指定此单元应在列出的单元之后启动。这有助于管理启动顺序。对于 Web 应用程序,你可能希望确保网络已启动。
ini After=network.targetRequires:类似于After,但表示更强的依赖关系。如果所需的单元失败,此单元将不会启动或将被停止。
ini Requires=docker.serviceWants:Requires的弱化形式。如果所需的单元失败或未找到,此单元仍会尝试启动。对于非关键依赖项,这通常比Requires更好。
ini Wants=syslog.target
[Service] 部分
此部分定义服务的执行参数,包括如何启动、停止以及其行为。
-
Type:定义进程启动类型。对于 systemd 如何监视你的服务至关重要。simple(默认):ExecStart命令是服务的主进程。systemd 在调用ExecStart后立即认为服务已启动。它期望进程在前台无限期运行。forking:ExecStart命令会 fork 出一个子进程,父进程退出。一旦父进程退出,systemd 就认为服务已启动。如果你的应用程序会自行守护化,请使用此选项。oneshot:ExecStart命令是一个一次性进程,完成后退出。适用于执行任务然后终止的脚本(例如,备份脚本)。notify:类似于simple,但服务在准备就绪时向 systemd 发送通知。需要libsystemd-dev和应用程序中的特定代码。idle:ExecStart命令仅在所有作业完成后执行,将执行推迟到系统大部分空闲时。
ini Type=simple -
ExecStart:服务启动时要执行的命令。这是此部分中最重要的指令。始终使用可执行文件或脚本的绝对路径。
ini ExecStart=/usr/bin/python3 /opt/my_app/app.py ExecStop:服务停止时要执行的命令(可选)。如果未指定,systemd 会向进程发送SIGTERM。
ini ExecStop=/usr/bin/pkill -f 'my_app/app.py'ExecReload:重新加载服务配置时要执行的命令(可选)。
ini ExecReload=/bin/kill -HUP $MAINPIDUser:服务进程将在其下运行的用户帐户。对安全至关重要;除非绝对必要,否则避免使用root。
ini User=myappuserGroup:服务进程将在其下运行的组帐户。
ini Group=myappgroupWorkingDirectory:执行命令的工作目录。
ini WorkingDirectory=/opt/my_appRestart:定义何时应自动重启服务。no(默认):从不重启。on-success:仅在服务正常退出时重启。on-failure:仅在服务以非零状态码退出或被信号杀死时重启。always:始终重启服务,无论退出状态如何。
ini Restart=on-failure
RestartSec:重启服务前等待的时间(例如,5s表示 5 秒)。
ini RestartSec=5sEnvironment:为执行的命令设置环境变量。
ini Environment="APP_ENV=production" "DEBUG=false"EnvironmentFile:从文件中读取环境变量。每行应为KEY=VALUE。
ini EnvironmentFile=/etc/default/my_appLimitNOFILE:设置服务允许的最大打开文件描述符数(例如,100000)。对于高并发应用程序很重要。
ini LimitNOFILE=65536
[Install] 部分
此部分定义服务如何在启动时自动启动。
WantedBy:指定“想要”此服务的目标单元。启用目标单元时,此服务将链接到其.wants目录,从而有效地使其与目标一起启动。multi-user.target:大多数服务器服务的标准目标,表示一个具有非图形多用户登录的系统。graphical.target:适用于需要图形界面的服务。
ini WantedBy=multi-user.target
RequiredBy:类似于WantedBy,但依赖性更强。如果启用了目标,此单元也将被启用,如果此单元失败,目标也将失败。
提示:对于大多数打算在服务器上后台运行的自定义服务,
Type=simple和WantedBy=multi-user.target是最常见和最合适的选择。
分步:创建和管理自定义 Systemd 服务
让我们创建一个实际示例:一个简单的 Python HTTP 服务器,用于从指定目录提供文件。我们将将其设置为 systemd 服务。
步骤 1:准备你的应用程序/脚本
首先,创建应用程序脚本。在此示例中,我们将使用简单的 Python HTTP 服务器。创建一个应用程序目录,例如 /opt/my_app,并将 app.py 放在其中。
# /opt/my_app/app.py
import http.server
import socketserver
import os
PORT = int(os.environ.get("PORT", 8000))
DIRECTORY = os.environ.get("DIRECTORY", os.getcwd())
class Handler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DIRECTORY, **kwargs)
print(f"Serving directory {DIRECTORY} on port {PORT}")
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("Server started.")
httpd.serve_forever()
创建目录和文件:
sudo mkdir -p /opt/my_app
sudo nano /opt/my_app/app.py
(粘贴 Python 代码)
确保脚本可执行(对于 python3 命令可选,但这是个好习惯):
sudo chmod +x /opt/my_app/app.py
出于安全原因,请考虑为你的服务创建一个专用用户:
sudo useradd --system --no-create-home myappuser
设置应用程序目录的适当所有权:
sudo chown -R myappuser:myappuser /opt/my_app
步骤 2:创建单元文件
现在,为我们的 Python 应用程序创建 systemd 单元文件。我们将其命名为 my_app.service。
sudo nano /etc/systemd/system/my_app.service
粘贴以下内容:
# /etc/systemd/system/my_app.service
[Unit]
Description=我的自定义 Python HTTP 服务器
Documentation=https://github.com/example/my_app
After=network.target
[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
Environment="PORT=8080" "DIRECTORY=/var/www/html"
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
注意:我们设置了
StandardOutput=journal和StandardError=journal,将服务的输出定向到 systemd 日志,这样就可以轻松地使用journalctl查看日志。
步骤 3:放置单元文件
如前所述,我们将单元文件放在了 /etc/systemd/system/ 中。这是自定义单元文件应存在的位置。
步骤 4:重新加载 Systemd 守护进程
创建或修改单元文件后,需要通知 systemd 更改。这通过重新加载 systemd 守护进程来完成:
sudo systemctl daemon-reload
步骤 5:启动服务
现在你可以启动服务了:
sudo systemctl start my_app.service
步骤 6:检查服务状态和日志
验证你的服务是否正常运行:
systemctl status my_app.service
示例输出(截断):
● my_app.service - My Custom Python HTTP Server
Loaded: loaded (/etc/systemd/system/my_app.service; disabled; vendor preset: enabled)
Active: active (running) since Tue 2023-10-26 10:30:00 UTC; 5s ago
Docs: https://github.com/example/my_app
Main PID: 12345 (python3)
Tasks: 1 (limit: 1100)
Memory: 6.5M
CPU: 45ms
CGroup: /system.slice/my_app.service
└─12345 /usr/bin/python3 /opt/my_app/app.py
Oct 26 10:30:00 yourhostname python3[12345]: Serving directory /var/www/html on port 8080
Oct 26 10:30:00 yourhostname python3[12345]: Server started.
要查看服务的日志,请使用 journalctl:
journalctl -u my_app.service -f
此命令显示 my_app.service 的日志,-f(follow)将实时显示新日志。
你也可以在浏览器或使用 curl 测试服务器,访问 http://localhost:8080(假设 /var/www/html 存在且包含一些文件)。
步骤 7:启用服务以自动启动
要使你的服务在每次系统启动时自动启动,你需要启用它:
sudo systemctl enable my_app.service
此命令将创建一个从 /etc/systemd/system/multi-user.target.wants/my_app.service 到 /etc/systemd/system/my_app.service 的符号链接。
步骤 8:停止和服务禁用
要停止正在运行的服务:
sudo systemctl stop my_app.service
要阻止服务在启动时自动启动(同时允许手动启动):
sudo systemctl disable my_app.service
如果你想完全移除服务,请先disable它,然后stop它,最后删除 /etc/systemd/system/ 中的 .service 文件并运行 sudo systemctl daemon-reload。
步骤 9:更新服务
如果你修改了 app.py 脚本或 my_app.service 单元文件,你需要更新 systemd 并重启服务:
- 编辑
/opt/my_app/app.py或/etc/systemd/system/my_app.service。 - 如果修改了单元文件,运行
sudo systemctl daemon-reload。 - 重启服务:
sudo systemctl restart my_app.service。
最佳实践和故障排除
- 绝对路径:始终对
ExecStart、WorkingDirectory和单元文件中的任何其他文件路径使用绝对路径。相对路径可能导致意外行为。 - 专用用户:在非特权、专用用户帐户(例如
myappuser)下运行服务,以增强安全性并限制潜在的损害。 - 清晰的日志记录:使用
StandardOutput=journal和StandardError=journal将服务输出定向到 systemd 日志。使用journalctl -u <service_name>查看日志。 - 依赖关系:仔细考虑
After、Wants和Requires,以确保你的服务相对于其依赖项(例如,网络、数据库)以正确的顺序启动。 - 测试更改:在启用服务以在启动时启动之前,通过手动启动和停止来彻底测试它。检查其状态和日志。
- 资源限制:使用
LimitNOFILE、LimitNPROC、MemoryLimit等指令,以防止失控的服务耗尽所有系统资源。 - 环境变量:使用
Environment=或EnvironmentFile=来配置可能在不同环境中更改或变化的配置值,而不是在单元文件或脚本中硬编码它们。 - 脚本中的错误处理:确保你的应用程序脚本能够优雅地处理错误。非零退出码将触发
Restart=on-failure。
警告:避免直接修改
/usr/lib/systemd/system/中的单元文件。任何更改都可能被包更新覆盖。请使用/etc/systemd/system/来自定义单元或覆盖。
结论
Systemd 单元文件是管理 Linux 系统上进程和应用程序的强大而灵活的机制。通过理解它们的结构和关键指令,你可以有效地标准化自定义服务的启动、停止和监视,从而提高系统稳定性并简化管理。从使用 ExecStart 定义启动命令,到使用 After 管理依赖关系,再到使用 WantedBy 启用自动启动,你现在拥有了将应用程序无缝集成到 systemd 生态系统的工具。这项基本技能对于维护健壮可靠的 Linux 部署至关重要。
继续探索 systemd 的高级功能,如计时器 (.timer)、套接字激活 (.socket) 和 cgroups,以实现更复杂的服务管理场景。