如何高效编写和管理自定义 Systemd 单元文件
掌握管理 Linux 服务的艺术,通过这份关于自定义 systemd 单元文件的全面指南。学习创建、配置和排查 `.service` 文件,利用 `ExecStart`、`WantedBy` 和 `Type` 等关键指令。本文提供逐步说明和实用示例,帮助您标准化应用程序启动、确保可靠运行,并将自定义进程无缝集成到 Linux 系统环境中。对于追求稳健服务管理的开发者和系统管理员至关重要。
如何高效编写和管理自定义 Systemd 单元文件
自定义 systemd 单元文件将终端中运行的命令转变为操作系统可以启动、停止、重启、记录和监控的服务。这种区别至关重要。在 shell 中运行的命令继承您的环境,并在会话结束时终止。而服务则拥有明确的用户、工作目录、重启策略、依赖关系、资源限制和日志。
这是我用于小型内部 API、工作进程、sidecar 脚本和一次性守护进程的实用方法:编写最简单正确的单元,以专用用户身份运行,将日志发送到 journal,并且仅在出现实际需求时才添加高级指令。
理解 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输出中。Description=我的自定义 Python Web 应用程序Documentation:指向服务文档的 URL(可选)。Documentation=https://example.com/docs/my-appAfter:指定此单元应在列出的单元 之后 启动。这有助于管理启动顺序。对于 Web 应用程序,您可能希望确保网络已启动。After=network.targetRequires:强依赖关系。如果所需的单元未能启动,此单元将不会启动。如果所需的单元被停止,此单元也可能被停止。Requires=docker.serviceWants:较弱的依赖关系。如果所需的单元失败或未找到,此单元仍会尝试启动。这通常是比Requires更好的默认选择。Wants=syslog.target
[Service] 部分
此部分定义服务的执行参数,包括如何启动、停止和运行。
Type:定义进程启动类型。对于 systemd 如何监控您的服务至关重要。simple(默认):ExecStart命令是服务的主进程。Systemd 在调用ExecStart后立即认为服务已启动。它期望进程在前台无限期运行。forking:ExecStart命令 fork 一个子进程,父进程退出。Systemd 在父进程退出后认为服务已启动。如果您的应用程序自行守护进程,请使用此类型。oneshot:ExecStart命令是一次性进程,完成后退出。适用于执行任务并终止的脚本(例如备份脚本)。notify:类似于simple,但服务在准备就绪时通知 systemd。这需要应用程序支持 systemd 通知。idle:ExecStart命令仅在所有作业完成后执行,将执行延迟到系统基本空闲时。
Type=simpleExecStart:服务启动时执行的命令。这是此部分中最重要的指令。始终使用可执行文件或脚本的绝对路径。ExecStart=/usr/bin/python3 /opt/my_app/app.pyExecStop:服务停止时执行的命令(可选)。如果未指定,systemd 会向进程发送SIGTERM。ExecStop=/usr/bin/pkill -f 'my_app/app.py'ExecReload:重新加载服务配置时执行的命令(可选)。ExecReload=/bin/kill -HUP $MAINPIDUser:服务进程将运行的用户帐户。对于安全性至关重要;除非绝对必要,否则避免使用root。User=myappuserGroup:服务进程将运行的组帐户。Group=myappgroupWorkingDirectory:执行命令的工作目录。WorkingDirectory=/opt/my_appRestart:定义何时应自动重启服务。no(默认):从不重启。on-success:仅在服务正常退出时重启。on-failure:仅在服务以非零状态码退出或被信号杀死时重启。always:始终重启服务,无论退出状态如何。
Restart=on-failureRestartSec:重启服务前等待的时间(例如5s表示 5 秒)。RestartSec=5sEnvironment:为执行的命令设置环境变量。Environment="APP_ENV=production" "DEBUG=false"EnvironmentFile:从文件读取环境变量。每行应为KEY=VALUE。EnvironmentFile=/etc/default/my_appLimitNOFILE:设置服务允许的最大打开文件描述符数量(例如100000)。对于高并发应用程序很重要。LimitNOFILE=65536
[Install] 部分
此部分定义如何启用服务以在启动时自动启动。
WantedBy:指定“想要”此服务的目标单元。当目标单元被启用时,此服务将被符号链接到其.wants目录中,从而使其随目标一起启动。multi-user.target:大多数服务器服务的标准目标,表示具有非图形多用户登录的系统。graphical.target:对于需要图形环境的服务。
WantedBy=multi-user.targetRequiredBy:类似于WantedBy,但依赖关系更强。如果目标被启用,此单元也会被启用,并且如果此单元失败,目标也会失败。
对于大多数旨在服务器后台运行的自定义服务,Type=simple 和 WantedBy=multi-user.target 是正确的起点。如果应用程序已经自行守护进程,要么禁用该行为,要么小心使用 Type=forking。前台进程更容易被 systemd 监控。
分步指南:创建和管理自定义 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"在端口 {PORT} 上提供目录 {DIRECTORY}")
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print("服务器已启动。")
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 journal,从而可以轻松使用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 - 我的自定义 Python HTTP 服务器
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]: 在端口 8080 上提供目录 /var/www/html
Oct 26 10:30:00 yourhostname python3[12345]: 服务器已启动。
要查看服务的日志,请使用 journalctl:
journalctl -u my_app.service -f
此命令显示 my_app.service 的日志,-f(跟随)将实时显示新日志。
您还可以从浏览器或 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。
实际服务的更安全模式
一个能工作的单元并不总是您想要维护多年的单元。这些模式可以防止常见错误:
- 在前台运行。 让 systemd 监控主进程。避免在
ExecStart中使用nohup、screen、tmux、后台&或应用程序守护进程模式。 - 保持
ExecStart直接。 如果您需要 shell 功能,如管道或变量扩展,请有意调用/bin/sh -c '...'。否则,直接运行可执行文件。 - 使用专用用户。 只需要读取
/opt/my_app并绑定到非特权端口的服务不应以root身份运行。 - 编辑单元后重新加载。 当单元文件更改时,需要
sudo systemctl daemon-reload。 - 将代码部署与单元更改分开。 如果只更改了 Python 代码,请重启服务。如果单元更改,请先重新加载 systemd。
排查新单元的问题
如果服务失败,请从以下命令开始:
systemctl status my_app.service
journalctl -u my_app.service -n 100 --no-pager
systemctl cat my_app.service
常见的失败通常很简单:
status=203/EXEC通常意味着可执行文件路径错误、文件丢失或文件不可执行。Permission denied通常意味着服务用户无法读取文件、进入目录、写入日志或绑定请求的端口。address already in use意味着另一个进程占用了该端口。使用sudo ss -tulpen | grep ':8080'检查。- 手动启动但在 systemd 下失败的服务通常依赖于环境变量、不同的工作目录或您主目录中的文件。
您可以以服务用户身份测试命令:
sudo -u myappuser /usr/bin/python3 /opt/my_app/app.py
这并非 systemd 环境的完美再现,但它能在您追究单元文件细节之前捕获明显的应用程序错误。
更面向生产的变体
对于长期运行的内部服务,我通常会添加一些防护措施:
[Unit]
Description=我的自定义 Python HTTP 服务器
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=myappuser
Group=myappuser
WorkingDirectory=/opt/my_app
EnvironmentFile=-/etc/my_app/my_app.env
ExecStart=/usr/bin/python3 /opt/my_app/app.py
Restart=on-failure
RestartSec=10s
TimeoutStopSec=30s
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/opt/my_app /var/log/my_app
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
ProtectSystem=full 使系统的大部分对服务只读,因此仅添加应用程序真正需要写入的目录到 ReadWritePaths=。一次测试一个加固指令。安全选项很有用,但无法读取其配置或写入其数据的服务将在启动时失败。
最佳实践和故障排除
- 绝对路径:始终在单元文件中为
ExecStart、WorkingDirectory和任何其他文件路径使用绝对路径。相对路径可能导致意外行为。 - 专用用户:在非特权、专用的用户帐户(例如
myappuser)下运行服务,以增强安全性并限制在受损情况下的潜在损害。 - 清晰的日志记录:利用
StandardOutput=journal和StandardError=journal将服务输出定向到 systemd journal。使用journalctl -u <service_name>查看日志。 - 依赖关系:仔细考虑
After、Wants和Requires,以确保您的服务相对于其依赖项(例如网络、数据库)以正确的顺序启动。 - 测试更改:在启用服务以在启动时启动之前,通过手动启动和停止服务来彻底测试它。检查其状态和日志。
- 资源限制:当服务具有已知限制或故障模式时,使用
LimitNOFILE、LimitNPROC和MemoryMax等指令。 - 环境变量:对于可能更改或在不同环境之间变化的配置值,使用
Environment=或EnvironmentFile=,而不是将其硬编码在单元文件或脚本中。 - 脚本中的错误处理:确保您的应用程序脚本优雅地处理错误。非零退出代码将触发
Restart=on-failure。
警告:避免直接修改
/usr/lib/systemd/system/中的单元文件。任何更改都可能被软件包更新覆盖。对于自定义单元或覆盖文件,请使用/etc/systemd/system/。
一个好的自定义单元在最理想的情况下是平淡无奇的:命令明确,用户非特权,重启行为有意图,日志易于查找。一旦这些稳固,systemd 定时器、套接字激活和更深的 cgroup 控制就是自然而然的下一步。