精通 Systemd:创建你的第一个自定义服务单元文件
通过创建自定义单元文件,学习 Systemd 服务管理的基础知识。本教程详细解析了 `[Unit]`、`[Service]` 和 `[Install]` 三个核心部分,并提供了逐步指导,教你如何使用 `systemctl` 在 Linux 上定义、启用、启动和验证一个基本的后台服务。
精通 Systemd:创建你的第一个自定义服务单元文件
当一个脚本或小型应用已经超出了终端会话、screen 窗口或脆弱的 cron 变通方案的范畴时,你就需要创建一个自定义的 systemd 服务单元。也许你有一个需要在崩溃后重启的工作进程。也许一个小型的内部 API 需要在网络就绪后启动。也许一个备份脚本应该作为一个受控的服务来运行,这样它的日志就能保存在日志中,操作员也能使用他们用于其他所有服务的相同 systemctl 命令。
Systemd 的有用之处并不在于单元文件有多复杂。而在于单元文件使进程变得明确:运行什么、以什么身份运行、何时启动、如何停止、日志存放在哪里,以及当它失败时 systemd 应该做什么。一旦这些决策被记录下来,服务就变得更容易运维了。
本教程从头开始构建一个小型服务。示例故意保持简单,但其中的模式与你用于后台工作进程、队列消费者、指标导出器或内部守护进程的模式是相同的。
从一个真实的命令开始,而不是单元文件
一个好的服务单元从一个已经可以手动运行的命令开始。在编写 systemd 配置之前,请确保你可以直接运行该程序,并理解它在前台做什么。
对于这个示例,创建一个小的报告脚本:
sudo install -d -o root -g root -m 0755 /opt/my-custom-service
sudo nano /opt/my-custom-service/reporter.sh
添加以下内容:
#!/usr/bin/env bash
set -euo pipefail
while true; do
echo "$(date --iso-8601=seconds) reporter heartbeat"
sleep 10
done
使其可执行并测试:
sudo chmod 0755 /opt/my-custom-service/reporter.sh
/opt/my-custom-service/reporter.sh
在看到几行输出后,使用 Ctrl-C 停止它。注意,脚本写入标准输出,而不是直接追加到 /var/log/reporter.log。这是有意为之。对于大多数自定义服务来说,让 systemd 将 stdout 和 stderr 捕获到日志中,比让每个脚本管理自己的日志文件权限、轮转和失败行为更简洁。
创建一个专用的服务用户
除非应用程序确实需要 root 权限,否则避免以 root 身份运行服务。一个心跳脚本不需要。一个 Web 应用通常也不需要。一个从队列读取并写入数据库的工作进程通常也不需要。
创建一个锁定的系统用户:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin reporter
如果你的发行版使用不同的 nologin 路径,请使用以下命令检查:
command -v nologin || command -v false
服务用户应该只拥有它需要写入的文件。在这个示例中,脚本通过 systemd 写入日志,因此它不需要拥有 /opt/my-custom-service。
编写服务单元
自定义的管理员管理系统单元通常位于 /etc/systemd/system/。供应商软件包单元通常位于 /usr/lib/systemd/system/ 或 /lib/systemd/system/,具体取决于发行版。尽可能不要直接编辑供应商单元文件;使用 /etc/systemd/system/ 存放你自己的单元,并使用 drop-in 文件进行覆盖。
创建单元:
sudo nano /etc/systemd/system/my-reporter.service
使用以下内容作为实用的第一个版本:
[Unit]
Description=My Custom Reporter Service
Documentation=man:systemd.service(5)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=reporter
Group=reporter
ExecStart=/opt/my-custom-service/reporter.sh
Restart=on-failure
RestartSec=5s
WorkingDirectory=/opt/my-custom-service
StandardOutput=journal
StandardError=journal
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
[Unit] 部分描述了关系。After=network-online.target 控制顺序;它本身不会拉取 network-online 目标。Wants=network-online.target 请求 systemd 也启动该目标。如果你的服务不需要网络,请删除这两行,使单元更简单。
[Service] 部分描述了进程。Type=simple 适用于不会自行 fork 到后台的前台进程。这是现代服务的常见情况。如果旧版守护进程会 fork、写入 PID 文件并将控制权返回给 shell,那么你可能需要 Type=forking,但不要仅仅因为这个词听起来更像守护进程就使用它。
ExecStart 应该是一个绝对路径。Shell 特性(如管道、重定向和 &&)不会被解释,除非你显式运行一个 shell,例如 ExecStart=/bin/bash -lc 'command one && command two'。当命令需要 shell 逻辑时,最好使用脚本;这样更容易测试和阅读。
Restart=on-failure 告诉 systemd 在异常退出后重启服务。它不会在干净的 systemctl stop 后重启。RestartSec=5s 防止紧密的重启循环对机器造成冲击。
这里的安全加固选项虽然适度但很有用。NoNewPrivileges=true 防止进程及其子进程通过 setuid 二进制文件或文件能力获得新权限。PrivateTmp=true 为服务提供一个私有的 /tmp 视图。这些对于简单服务通常是安全的,但请在实际应用程序中测试它们,因为某些软件可能期望共享的临时路径。
加载并启动单元
在添加或更改单元文件后,重新加载 systemd 的管理器配置:
sudo systemctl daemon-reload
现在启动服务:
sudo systemctl start my-reporter.service
检查其状态:
systemctl status my-reporter.service
你应该看到 Active: active (running)。如果失败了,不要猜测。读取日志:
journalctl -u my-reporter.service -n 50 --no-pager
在测试时跟踪实时日志:
journalctl -u my-reporter.service -f
如果脚本路径错误、权限缺失、用户不存在或命令立即退出,systemd 通常会在日志中明确说明。
启用开机自启
启动服务和启用服务是不同的操作。start 立即运行它。enable 将其连接到启动目标,以便在将来启动时启动。
sudo systemctl enable my-reporter.service
在单元经过测试后,你可以通过一个命令同时完成这两项操作:
sudo systemctl enable --now my-reporter.service
要查看它是否已启用:
systemctl is-enabled my-reporter.service
让故障更容易诊断
最常见的首次服务故障是穿着 systemd 外衣的普通 Linux 问题。
如果你看到 status=203/EXEC,说明 systemd 无法执行该命令。检查路径、可执行位、shebang 行和行尾。从 Windows 复制过来的带有 CRLF 行尾的脚本即使看起来正常也可能失败。
如果你看到权限错误,请记住服务是以 reporter 身份运行的,而不是你的 shell 用户。使用以下命令测试:
sudo -u reporter /opt/my-custom-service/reporter.sh
如果服务启动后立即停止,进程可能退出了。Type=simple 期望命令持续运行。一次性设置命令应使用 Type=oneshot,而不是 simple。
如果日志丢失,请检查应用程序是写入文件还是 stdout/stderr,或者它是否在内部更改了用户。对于大多数小型服务,写入 stdout 是最不令人惊讶的选择。
有用的管理命令
一旦单元就位,日常操作就很简单了:
sudo systemctl start my-reporter.service
sudo systemctl stop my-reporter.service
sudo systemctl restart my-reporter.service
sudo systemctl reload my-reporter.service
systemctl status my-reporter.service
journalctl -u my-reporter.service --since "1 hour ago"
systemctl cat my-reporter.service
systemctl show my-reporter.service -p User -p Restart -p ExecStart
systemctl cat 在具有 drop-in 覆盖文件的机器上特别有用,因为它显示了 systemd 正在读取的有效单元片段。
一个自定义单元文件不需要很巧妙。它需要的是枯燥、明确和可测试。先手动让命令工作,以专用用户身份运行它,编写能准确描述服务的最小单元,重新加载 systemd,并在出现问题时使用日志。这个工作流程可以从一个玩具报告脚本扩展到真正的生产守护进程。
干净地添加环境和配置
迟早,服务需要配置:一个端口、一个数据库 URL、一个功能标志或一个路径。当这些值因环境而异时,避免将它们埋在单元文件中。一个常见的模式是环境文件:
sudo nano /etc/my-reporter.env
示例:
REPORT_INTERVAL=10
REPORT_LABEL=production
如果文件包含任何敏感信息,请锁定它:
sudo chown root:reporter /etc/my-reporter.env
sudo chmod 0640 /etc/my-reporter.env
然后在单元中引用它:
[Service]
EnvironmentFile=/etc/my-reporter.env
ExecStart=/opt/my-custom-service/reporter.sh
在脚本中,使用默认值读取变量:
interval="${REPORT_INTERVAL:-10}"
label="${REPORT_LABEL:-default}"
对于机密信息,请小心。根据系统配置和权限,环境变量可能通过进程检查或服务元数据暴露。对于高度敏感的值,最好使用适当的机密管理器、具有严格权限的凭证文件,或者如果你的发行版支持,使用 systemd 较新的凭证功能。重要的习惯是刻意做出决定,而不是为了方便而将密码散布到单元文件中。
使用 drop-in 覆盖文件进行本地更改
如果软件包安装了一个单元,而你需要更改一个设置,不要编辑供应商文件。使用 drop-in 文件:
sudo systemctl edit my-reporter.service
这会在 /etc/systemd/system/my-reporter.service.d/ 下打开一个覆盖文件。例如:
[Service]
RestartSec=15s
保存后重新加载并重启:
sudo systemctl daemon-reload
sudo systemctl restart my-reporter.service
检查合并后的结果:
systemctl cat my-reporter.service
Drop-in 文件很重要,因为软件包更新可能会替换供应商单元。你在 /etc 中的覆盖文件保持可见且有意为之。
考虑关闭行为
启动只是生命周期的一半。服务也应该干净地停止。默认情况下,systemd 发送 SIGTERM,等待,然后如果进程没有退出,可能会发送 SIGKILL。对于许多简单服务来说,这没问题。对于队列工作进程、上传处理器和数据库写入器,你可能需要处理终止信号,以便进程安全地完成或放弃当前工作。
你可以调整超时:
[Service]
TimeoutStopSec=30s
KillSignal=SIGTERM
除非你有理由,否则不要设置极长的停止超时。长时间的关闭会减慢部署、重启和事件恢复的速度。一个工作进程通常应该停止接受新工作,完成正在处理的项目,并在有限的时间内退出。
防止嘈杂的重启循环
Restart=on-failure 很有用,但一个损坏的服务仍然可以反复重启。当故障模式可能很嘈杂时,添加限制:
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
这告诉 systemd 在间隔时间内失败次数过多后停止尝试。当你修复问题后,重置失败状态:
sudo systemctl reset-failed my-reporter.service
sudo systemctl start my-reporter.service
这个命令在测试期间也很有用。即使你已经更正了脚本或权限,服务也可能保持失败状态。
在依赖单元之前进行验证
Systemd 有一个有用的验证器:
systemd-analyze verify /etc/systemd/system/my-reporter.service
它不会捕获所有应用程序问题,但可以捕获语法错误、你的 systemd 版本中未知的设置以及一些顺序问题。在较大的编辑后或在发行版之间复制单元时运行它。Systemd 特性因版本而异,因此在新 Fedora 服务器上有效的安全加固选项可能不存在于较旧的企业发行版上。
当启动顺序令人困惑时,也要检查单元的依赖关系视图:
systemctl list-dependencies my-reporter.service
systemctl list-dependencies --reverse my-reporter.service
第一个命令显示服务拉取或依赖的内容。反向视图显示依赖于它的内容。当服务意外启动或禁用某个单元影响另一个单元时,这很有用。
决定服务是系统级还是用户级
本文使用 /etc/systemd/system/ 下的系统服务。这是机器服务的正确选择,这些服务应在启动时启动并独立于登录会话运行。Systemd 也支持用户服务,通常使用 systemctl --user 管理,用于每个用户的后台进程。
不要仅仅为了避免 sudo 而将基础设施守护进程用作户服务。用户服务具有不同的生命周期规则、环境处理和登录行为。对于应用程序工作进程、导出器和主机级代理,使用具有专用最小权限用户的系统服务通常更容易推理。
保持第一个单元简单
很容易添加你在网上找到的每个安全加固指令:私有设备、只读路径、系统调用过滤器、能力边界、命名空间限制等等。这些都是有价值的工具,但要在服务工作后逐个添加。当一个受到严格限制的服务无法启动时,初学者通常无法判断是单元错误、应用程序错误,还是沙箱设置阻止了它需要的文件。
一个好的生产路径是渐进的:工作服务、专用用户、可靠的日志记录、重启策略、基本安全加固,然后在你有一个证明应用程序仍然正常运行的测试之后,再进行更强的沙箱化。