如何有效地编写和管理自定义 Systemd 单元文件

通过这份关于自定义 Systemd 单元文件的综合指南,掌握管理 Linux 服务的艺术。学习创建、配置和排查 `.service` 文件,并利用 `ExecStart`、`WantedBy` 和 `Type` 等关键指令。本文提供了分步说明和实际示例,使您能够标准化应用程序启动、确保可靠运行,并将您的自定义进程无缝集成到 Linux 系统环境中。对于旨在实现健壮服务管理的开发人员和系统管理员至关重要。

38 浏览量

如何有效编写和管理自定义 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-app
  • After:指定此单元应在列出的单元之后启动。这有助于管理启动顺序。对于 Web 应用程序,你可能希望确保网络已启动。
    ini After=network.target
  • Requires:类似于 After,但表示更强的依赖关系。如果所需的单元失败,此单元将不会启动或将被停止。
    ini Requires=docker.service
  • WantsRequires 的弱化形式。如果所需的单元失败或未找到,此单元仍会尝试启动。对于非关键依赖项,这通常比 Requires 更好。
    ini Wants=syslog.target

[Service] 部分

此部分定义服务的执行参数,包括如何启动、停止以及其行为。

  • Type:定义进程启动类型。对于 systemd 如何监视你的服务至关重要。

    • simple (默认)ExecStart 命令是服务的主进程。systemd 在调用 ExecStart 后立即认为服务已启动。它期望进程在前台无限期运行。
    • forkingExecStart 命令会 fork 出一个子进程,父进程退出。一旦父进程退出,systemd 就认为服务已启动。如果你的应用程序会自行守护化,请使用此选项。
    • oneshotExecStart 命令是一个一次性进程,完成后退出。适用于执行任务然后终止的脚本(例如,备份脚本)。
    • notify:类似于 simple,但服务在准备就绪时向 systemd 发送通知。需要 libsystemd-dev 和应用程序中的特定代码。
    • idleExecStart 命令仅在所有作业完成后执行,将执行推迟到系统大部分空闲时。

    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 $MAINPID
  • User:服务进程将在其下运行的用户帐户。对安全至关重要;除非绝对必要,否则避免使用 root
    ini User=myappuser
  • Group:服务进程将在其下运行的组帐户。
    ini Group=myappgroup
  • WorkingDirectory:执行命令的工作目录。
    ini WorkingDirectory=/opt/my_app
  • Restart:定义何时应自动重启服务。
    • no (默认):从不重启。
    • on-success:仅在服务正常退出时重启。
    • on-failure:仅在服务以非零状态码退出或被信号杀死时重启。
    • always:始终重启服务,无论退出状态如何。
      ini Restart=on-failure
  • RestartSec:重启服务前等待的时间(例如,5s 表示 5 秒)。
    ini RestartSec=5s
  • Environment:为执行的命令设置环境变量。
    ini Environment="APP_ENV=production" "DEBUG=false"
  • EnvironmentFile:从文件中读取环境变量。每行应为 KEY=VALUE
    ini EnvironmentFile=/etc/default/my_app
  • LimitNOFILE:设置服务允许的最大打开文件描述符数(例如,100000)。对于高并发应用程序很重要。
    ini LimitNOFILE=65536

[Install] 部分

此部分定义服务如何在启动时自动启动。

  • WantedBy:指定“想要”此服务的目标单元。启用目标单元时,此服务将链接到其 .wants 目录,从而有效地使其与目标一起启动。
    • multi-user.target:大多数服务器服务的标准目标,表示一个具有非图形多用户登录的系统。
    • graphical.target:适用于需要图形界面的服务。
      ini WantedBy=multi-user.target
  • RequiredBy:类似于 WantedBy,但依赖性更强。如果启用了目标,此单元也将被启用,如果此单元失败,目标也将失败。

提示:对于大多数打算在服务器上后台运行的自定义服务,Type=simpleWantedBy=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=journalStandardError=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 并重启服务:

  1. 编辑 /opt/my_app/app.py/etc/systemd/system/my_app.service
  2. 如果修改了单元文件,运行 sudo systemctl daemon-reload
  3. 重启服务:sudo systemctl restart my_app.service

最佳实践和故障排除

  • 绝对路径:始终对 ExecStartWorkingDirectory 和单元文件中的任何其他文件路径使用绝对路径。相对路径可能导致意外行为。
  • 专用用户:在非特权、专用用户帐户(例如 myappuser)下运行服务,以增强安全性并限制潜在的损害。
  • 清晰的日志记录:使用 StandardOutput=journalStandardError=journal 将服务输出定向到 systemd 日志。使用 journalctl -u <service_name> 查看日志。
  • 依赖关系:仔细考虑 AfterWantsRequires,以确保你的服务相对于其依赖项(例如,网络、数据库)以正确的顺序启动。
  • 测试更改:在启用服务以在启动时启动之前,通过手动启动和停止来彻底测试它。检查其状态和日志。
  • 资源限制:使用 LimitNOFILELimitNPROCMemoryLimit 等指令,以防止失控的服务耗尽所有系统资源。
  • 环境变量:使用 Environment=EnvironmentFile= 来配置可能在不同环境中更改或变化的配置值,而不是在单元文件或脚本中硬编码它们。
  • 脚本中的错误处理:确保你的应用程序脚本能够优雅地处理错误。非零退出码将触发 Restart=on-failure

警告:避免直接修改 /usr/lib/systemd/system/ 中的单元文件。任何更改都可能被包更新覆盖。请使用 /etc/systemd/system/ 来自定义单元或覆盖。

结论

Systemd 单元文件是管理 Linux 系统上进程和应用程序的强大而灵活的机制。通过理解它们的结构和关键指令,你可以有效地标准化自定义服务的启动、停止和监视,从而提高系统稳定性并简化管理。从使用 ExecStart 定义启动命令,到使用 After 管理依赖关系,再到使用 WantedBy 启用自动启动,你现在拥有了将应用程序无缝集成到 systemd 生态系统的工具。这项基本技能对于维护健壮可靠的 Linux 部署至关重要。

继续探索 systemd 的高级功能,如计时器 (.timer)、套接字激活 (.socket) 和 cgroups,以实现更复杂的服务管理场景。