59·进程管理高级

编写 systemd Unit 文件

systemdunit-files

第59课:编写 systemd Unit 文件

所属模块:进程管理 难度:高级 标签:systemd, unit-files 上一课回顾systemctl 服务管理 下一课预告journalctl 日志查看


1. 学习目标

通过本课程的学习,你将能够:

  • 理解 systemd Unit 文件的基本结构和语法。
  • 编写一个用于管理自定义应用程序(如脚本、二进制程序)的 .service Unit 文件。
  • 掌握关键的 Service 段落指令,以控制进程的启动、重启行为和环境。
  • 配置服务的依赖关系、启动顺序和安装信息。
  • 验证、启用并测试自定义的 systemd 服务。

2. 核心概念

在上一课中,我们学习了如何使用 systemctl 命令来管理(启动、停止、查看状态)由 systemd 提供的服务。但这些服务是如何被 systemd 识别和管理的呢?答案就是 Unit 文件

你可以将 Unit 文件想象成一份给 systemd 的 “说明书”。它以纯文本形式定义了一个服务(或一个挂载点、设备、套接字等)应如何运行。systemd 在启动时或被要求时,会读取这些说明书,并严格按照指令来管理对应的进程。

一个典型的 .service Unit 文件主要包含三个部分:

  • [Unit] 段落:提供通用的描述信息,如服务名称、描述,并定义与其他 Unit 的依赖和顺序关系(例如,必须在网络就绪后才能启动)。
  • [Service] 段落:核心部分,定义服务如何运行。包括启动命令、进程类型、重启策略、工作目录、环境变量等。
  • [Install] 段落:定义当服务被 “启用”enable)时应如何链接。通常指定服务在哪个“运行级别”或“目标”下启动,最常见的是 WantedBy=multi-user.target

3. 代码示例

让我们通过一个具体的例子来学习。假设我们有一个简单的 Python HTTP 服务器脚本,我们希望将它作为一个系统服务来管理。

第一步:创建应用程序脚本

创建一个简单的 Python 脚本,用于提供一个 Web 服务页面。

文件路径:/usr/local/bin/my-simple-web.py

#!/usr/bin/env python3
"""
一个简单的Python HTTP服务器,用于演示。
"""
import http.server
import socketserver
import os
import signal
import sys

# 设置工作目录和端口
PORT = 8000
# 为了简单,我们让脚本在 /tmp 目录下运行并生成文件
os.chdir("/tmp")
# 写入一个简单的HTML文件作为首页
with open("index.html", "w") as f:
    f.write("<html><body><h1>Hello from my systemd service!</h1></body></html>")

# 定义处理器
Handler = http.server.SimpleHTTPRequestHandler

# 创建并启动服务器
with socketserver.TCPServer(("", PORT), Handler) as httpd:
    print(f"Serving on port {PORT}", flush=True)
    # 优雅地处理停止信号
    def signal_handler(sig, frame):
        print("Shutting down server...", flush=True)
        httpd.shutdown()
        sys.exit(0)
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    httpd.serve_forever()

第二步:创建 systemd Unit 文件

这是本课的重点。我们为上述 Python 服务创建 Unit 文件。

文件路径:/etc/systemd/system/my-simple-web.service

# /etc/systemd/system/my-simple-web.service
# 描述:一个用于演示的简单Python HTTP服务

[Unit]
Description=My Simple Python HTTP Server
# 确保网络已经就绪后才启动我们的服务
After=network.target
# 声明我们的服务依赖网络(可选,但更明确)
Requires=network.target

[Service]
# 指定服务类型:`simple` 表示主进程就是由 ExecStart 启动的进程
Type=simple
# 执行启动命令的完整路径。我们使用 python3 解释器来运行我们的脚本。
# 注意:`/usr/bin/env python3` 路径在 Unit 文件中可能不可靠,最好用 `which python3` 查找绝对路径。
ExecStart=/usr/bin/python3 /usr/local/bin/my-simple-web.py
# 设置工作目录。脚本中 os.chdir("/tmp") 会生效,但这里设置可以覆盖或确认。
WorkingDirectory=/tmp
# 定义重启策略:当进程异常退出(非正常信号如 SIGTERM, SIGINT)时,总是尝试重启。
Restart=on-failure
# 定义重启前的等待时间(秒)。
RestartSec=5
# 定义服务启动前等待的超时时间。
TimeoutStartSec=30
# 定义服务停止的超时时间。
TimeoutStopSec=10
# 定义标准输出和错误输出的日志标识符,便于用 journalctl 按名字过滤。
SyslogIdentifier=my-simple-web
# 定义进程运行的用户和组(安全考虑,避免以 root 运行不必要的服务)。
# User=nobody
# Group=nogroup

[Install]
# 定义当服务被 `systemctl enable` 时,它希望被哪个目标(运行级别)依赖。
# `multi-user.target` 类似于传统的运行级别 3,是一个非图形的多用户命令行环境。
WantedBy=multi-user.target

第三步:验证、启用和启动服务

# 1. 重新加载 systemd 配置,让它知道我们添加了新文件
sudo systemctl daemon-reload

# 2. 验证 Unit 文件语法是否正确(虽然不是必须的,但推荐)
systemd-analyze verify /etc/systemd/system/my-simple-web.service

# 3. 启动服务
sudo systemctl start my-simple-web.service

# 4. 查看服务状态
sudo systemctl status my-simple-web.service

# 5. 查看服务的实时日志
sudo journalctl -u my-simple-web.service -f

# 6. 测试服务是否正常工作
curl http://localhost:8000
# 应输出:<html><body><h1>Hello from my systemd service!</h1></body></html>

# 7. (可选) 设置服务开机自启
sudo systemctl enable my-simple-web.service

4. 实践练习

练习1:修改现有服务配置

  1. 修改我们创建的 my-simple-web.service 文件,将监听端口从 8000 改为 9000。
  2. 同时,将运行用户改为 nobody(取消 [Service] 段中 UserGroup 行的注释)。
  3. 重新加载配置并重启服务。
  4. 使用 curl http://localhost:9000 测试,并用 systemctl status 查看运行用户是否已变更。

练习2:为另一个脚本创建服务

  1. 创建一个新的Bash脚本 /usr/local/bin/heartbeat.sh,内容如下:
    #!/bin/bash
    while true; do
        echo "Heartbeat at $(date)" >> /tmp/heartbeat.log
        sleep 10
    done
    
  2. 为这个脚本编写一个对应的 Unit 文件 /etc/systemd/system/heartbeat.service
  3. 要求:
    • 描述为 “Heartbeat Logger Service”。
    • 服务类型为 simple
    • 确保脚本有执行权限(chmod +x)。
    • 设置当服务失败时,在 15 秒后重启。
    • 将标准输出也重定向到系统日志(默认行为)。
  4. 启用并启动该服务,然后检查 /tmp/heartbeat.log 文件是否每 10 秒追加一条记录。

练习3:理解依赖与顺序

  1. 思考并回答:如果我们把 my-simple-web.service 中的 After=network.target 注释掉,服务可能会在什么情况下启动失败?为什么?

5. 常见错误

  1. 路径错误

    • ExecStart 中的命令或脚本路径不是绝对路径,导致 systemd 找不到可执行文件。
    • 工作目录 (WorkingDirectory) 不存在,导致应用程序启动失败。
    • 解决:始终使用 which <command>realpath <script> 获取绝对路径。
  2. 权限不足

    • Unit 文件本身权限不对(应由 root 拥有,权限为 644)。
    • ExecStart 指向的脚本或程序没有可执行权限 (chmod +x)。
    • 以非 root 用户(如 nobody)运行时,该用户无权访问脚本、工作目录或需要的资源(如端口 <1024)。
    • 解决:检查文件权限和所有权。使用 sudo -u nobody <command> 提前测试。
  3. 依赖与顺序混淆

    • 混淆 RequiresAfterRequires 是强依赖,如果依赖的 Unit 启动失败,本 Unit 也会失败。After 只是声明启动顺序,但不强求依赖存在。
    • 解决:通常 RequiresAfter 一起使用。对于可选的弱依赖,可使用 WantsAfter
  4. 服务类型 (Type) 设置错误

    • 最常见的错误是将 forking 类型的程序(如传统守护进程,自己 fork 到后台)设置为 Type=simple,导致 systemd 认为进程已退出而不断重启。
    • 解决:确定你的程序是前台运行(用 simpleexec)还是后台运行(用 forking)。
  5. 忘记 daemon-reload

    • 修改了 Unit 文件后,没有执行 sudo systemctl daemon-reload,导致更改未被 systemd 识别。
    • 解决:每次编辑 Unit 文件后,务必执行此命令。

6. 小结

本节课我们深入学习了如何通过编写 Unit 文件来将自定义应用程序纳入 systemd 的管理体系。关键要点回顾:

  • Unit 文件是核心:它是 systemd 理解和管理服务的配置蓝图。
  • 三大段落[Unit] 定义描述和依赖关系,[Service] 定义运行细节,[Install] 定义安装链接。
  • 关键指令
    • ExecStart:启动命令。
    • Type:服务进程类型(simple, forking, oneshot等)。
    • Restart:重启策略(on-failure, always)。
    • After/Requires/Wants:控制启动顺序和依赖。
  • 工作流程:编写文件 → daemon-reloadstart/enablestatus/journalctl 查看。
  • 安全与健壮性:考虑使用非特权用户运行服务、设置合理的超时和重启策略。

掌握 Unit 文件的编写,意味着你能够将几乎任何应用程序或脚本转化为易于管理、可监控、具备自愈能力的系统服务,这是 Linux 系统管理员和 DevOps 工程师的核心技能之一。

练习编辑器

rust
Loading...

继续学习

完成本课后,建议继续学习下一课「journalctl 日志查看」 以巩固所学知识。