精通 Systemd 服务文件:综合指南

创建可靠的systemd服务文件,包含正确的单元节、重启行为、日志、安全性和定时器。

掌握Systemd服务文件:全面指南

Systemd服务文件告诉Linux如何启动、停止、重启和监控你的应用程序。如果你的服务可以手动启动但在开机时失败、重启过于频繁,或者日志写入错误的位置,通常需要检查单元文件。

本指南专注于从头创建和配置systemd服务单元文件。你将看到核心部分、一个可用的Python服务示例、常见的故障排查命令,以及一些在生产环境中值得使用的安全性和资源控制措施。

理解Systemd单元文件

Systemd使用单元文件来描述各种系统资源,如服务、套接字、设备、挂载点等。服务单元文件通常以.service扩展名结尾,定义了systemd如何管理特定的守护进程或应用程序。

这些文件按节组织,每个节包含表示配置指令的键值对。我们将重点关注的三个主要节是[Unit][Service][Install]

Systemd服务文件的结构

一个典型的systemd服务文件具有以下结构:

[Unit]
Description=服务的简要描述。
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/my_application --config /etc/my_app.conf
Restart=on-failure
User=myuser
Group=mygroup

[Install]
WantedBy=multi-user.target

让我们分解每个节及其常见指令:

[Unit]

此节提供单元的元数据,并定义其与其他单元的关系。它用于依赖关系和排序。

  • Description=:服务的人类可读名称。你将在systemctl status输出中看到它。
  • Documentation=:服务文档的URL或路径。
  • Requires=:定义强依赖关系。如果此处列出的单元启动失败,此单元也将启动失败。
  • Wants=:定义弱依赖关系。如果此处列出的单元启动失败,此单元仍将尝试启动。
  • Before=:确保此单元在列出的单元之前启动。
  • After=:仅控制排序。例如,After=network.target在基本网络目标之后启动此单元,但不保证外部连接。依赖网络的服务可能需要After=network-online.target以及发行版的wait-online服务。
  • Conflicts=:如果此处列出的单元启动,此单元将被停止,反之亦然。

[Service]

此节配置服务本身的行为。它定义了如何启动、停止和管理进程。

  • Type=:指定进程启动类型。常见值包括:

    • simple(默认):主进程是ExecStart=中指定的进程。Systemd假定服务在ExecStart=进程被fork后立即启动。
    • forkingExecStart=进程fork一个子进程,父进程退出。Systemd在父进程退出时认为服务已启动。通常需要为此类型指定PIDFile=
    • oneshot:类似于simple,但进程在完成工作后预期退出。适用于设置脚本。
    • notify:守护进程在成功启动时向systemd发送通知消息。这是支持此功能的现代守护进程的首选类型。
    • dbus:服务获取一个D-Bus名称。
  • ExecStart=:启动服务的命令。对于大多数服务类型,使用一个ExecStart=命令。对于Type=oneshot,多个ExecStart=行是有效的,它们将顺序执行。

  • ExecStop=:停止服务的命令。

  • ExecReload=:在不重启的情况下重新加载服务配置的命令。

  • Restart=:定义何时应自动重启服务。常见值:

    • no(默认):从不重启。
    • on-success:仅在服务正常退出(退出码0)时重启。
    • on-failure:如果服务以非零退出码退出、被信号终止或超时,则重启。
    • on-abnormal:如果被信号终止或超时,则重启。
    • on-abort:仅当被信号不干净地终止时重启。
    • always:无论退出状态如何,始终重启。
  • RestartSec=:重启服务前休眠的时间(默认100毫秒)。

  • User=:运行服务的用户。

  • Group=:运行服务的组。

  • WorkingDirectory=:执行命令前切换到的目录。

  • Environment=:为服务设置环境变量。

  • EnvironmentFile=:从文件读取环境变量。

  • PIDFile=:PID文件的路径(通常与Type=forking一起使用)。

  • StandardOutput= / StandardError=:控制stdout和stderr的去向,如journalnullinherit。在常见的基于systemd的发行版上,除非管理器默认值已更改,否则服务输出通常进入日志。

[Install]

此节定义如何启用或禁用单元,通常通过创建符号链接。

  • WantedBy=:指定在启用时应该“想要”此服务的目标。常见值:
    • multi-user.target:适用于应在系统进入多用户命令行状态时启动的服务。
    • graphical.target:适用于应在系统进入图形登录状态时启动的服务。

创建你的第一个Systemd服务文件

让我们为一个假设的Python脚本my_app.py(位于/opt/my_app/my_app.py)创建一个简单的服务文件。

1. 创建服务文件:

自定义应用程序的服务文件通常放在/etc/systemd/system/中。让我们将文件命名为my_app.service

# 如果目录不存在,则创建
sudo mkdir -p /etc/systemd/system/

# 使用文本编辑器创建服务文件
sudo nano /etc/systemd/system/my_app.service

2. 将以下内容添加到my_app.service

[Unit]
Description=我的自定义Python应用程序
After=network.target

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/my_app/
ExecStart=/usr/bin/python3 /opt/my_app/my_app.py
Restart=on-failure

[Install]
WantedBy=multi-user.target

示例说明:

  • Description:清晰标识我们的应用程序。
  • After=network.target:确保在启动前网络可用。
  • Type=simple:假设my_app.py是主进程且不fork。
  • User=appuserGroup=appgroup:指定应用程序运行的用户和组。*确保这些用户和组在你的系统上存在并具有适当的权限。*你可能需要创建它们:
    sudo groupadd appgroup
    sudo useradd -r -g appgroup appuser
    sudo chown -R appuser:appgroup /opt/my_app/
    
  • WorkingDirectory:设置脚本的上下文。
  • ExecStart:运行Python脚本的命令。确保/usr/bin/python3是Python解释器的正确路径,并且脚本是可执行的。
  • Restart=on-failure:如果脚本崩溃,systemd将尝试重启它。
  • WantedBy=multi-user.target:此服务将在系统启动到多用户环境时自动启动。

3. 重新加载systemd管理器配置:

创建或修改服务文件后,必须告诉systemd重新加载其配置。

sudo systemctl daemon-reload

4. 启用并启动服务:

  • 启用:使服务在开机时自动启动。
    sudo systemctl enable my_app.service
    
  • 启动:立即启动服务。
    sudo systemctl start my_app.service
    

5. 检查服务状态:

要验证服务是否正在运行并查看潜在错误:

sudo systemctl status my_app.service

如果有问题,status命令通常会显示错误消息或来自journald的日志。

6. 查看日志:

Systemd与journald集成用于日志记录。你可以使用以下命令查看服务的日志:

sudo journalctl -u my_app.service

你还可以实时跟踪日志:

sudo journalctl -f -u my_app.service

其他有用命令

  • 停止服务sudo systemctl stop my_app.service
  • 重启服务sudo systemctl restart my_app.service
  • 重新加载配置(如果应用程序支持)sudo systemctl reload my_app.service
  • 禁用开机自启sudo systemctl disable my_app.service

高级配置和最佳实践

安全考虑

  • 以非root用户运行服务:除非绝对必要,否则始终指定User=Group=。这遵循最小权限原则。
  • 隔离服务:考虑使用沙箱功能,如PrivateTmp=trueProtectSystem=strictProtectHome=trueNoNewPrivileges=true。请与你的应用程序一起测试它们,因为它们可能会阻止合法的文件写入。
    • PrivateTmp=true:为服务提供其私有的/tmp/var/tmp目录。
    • ProtectSystem=strict:使服务的大部分文件系统变为只读。使用ReadWritePaths=指定服务必须写入的目录。
    • NoNewPrivileges=true:防止服务获得新权限。

处理复杂启动

  • Type=forkingPIDFile=:对于fork的旧应用程序,确保PIDFile=指向正确的文件。
  • Type=notify:如果你的应用程序支持,这是systemd了解其真正就绪的最可靠方式。
  • ExecStartPre=ExecStartPost=:在ExecStart=之前和之后运行的命令。适用于设置或清理任务。

资源控制

Systemd允许你限制资源使用:

  • CPUWeight=:服务的相对CPU权重。
  • MemoryMax=:服务可以使用的最大内存。
  • IOWeight=:在支持的内核和cgroup设置下的相对I/O权重。

示例:

[Service]
# ... 其他指令 ...
MemoryMax=512M
CPUWeight=50

定时器与Cron

Systemd定时器提供了传统cron作业的现代替代方案。它们更灵活,并且与systemd的日志记录和依赖管理集成得更好。

  • Cron:在crontab文件中定义的计划任务。
  • Systemd定时器(.timer单元):这些单元调度.service单元。你定义一个.timer文件,指定相应的.service文件何时运行。

示例:

要在每天凌晨3点运行一个脚本:

  1. my_script.service:要运行的服务。

    [Unit]
    Description=我的每日脚本
    
    [Service]
    Type=oneshot
    ExecStart=/opt/my_scripts/run_daily.sh
    User=scriptuser
    
  2. my_script.timer:调度服务的定时器。

    [Unit]
    Description=每天运行一次我的每日脚本
    
    [Timer]
    # 每天03:00运行
    OnCalendar=*-*-* 03:00:00
    # 如果机器关闭时错过了计划时间,则在启动后尽快运行。
    Persistent=true
    
    [Install]
    WantedBy=timers.target
    

使用方法:

  • 将两个文件放在/etc/systemd/system/中。
  • 运行sudo systemctl daemon-reload
  • 启用并启动定时器:sudo systemctl enable my_script.timersudo systemctl start my_script.timer

定时器提供了诸如Persistent=true(启动时运行错过的作业)、日历事件(如hourlydailyweekly)以及与journalctl更好的集成等优势。

常见问题排查

  • 服务未启动:检查systemctl status <service_name>journalctl -u <service_name>。查找拼写错误、路径不正确、缺少依赖项或权限错误。
  • Type=不正确:如果服务立即失败或挂起,Type=可能错误。尝试simpleforking,如果使用forking,确保PIDFile正确。
  • 权限被拒绝:确保指定的User=Group=对必要的文件和目录具有读/写访问权限。
  • 环境变量:如果你的应用程序依赖特定的环境变量,确保使用Environment=EnvironmentFile=正确设置它们。
  • 依赖关系:验证After=Wants=Requires=是否符合你的意图。After=排序启动;它本身不会拉入另一个单元。

在生产主机上启用新单元之前,运行:

sudo systemd-analyze verify /etc/systemd/system/my_app.service

这会在你依赖服务启动之前捕获许多语法和指令错误。

关键要点

编写准确描述应用程序的最小服务文件,然后有意识地添加重启策略、日志记录、安全限制和资源限制。每次更改后,运行systemctl daemon-reload,验证单元,并在信任其用于生产之前检查systemctl statusjournalctl -u