LabHub
学习 学习路径 课程

RHEL 系管理

把 systemd unit 文件写准

在 LabHub 中继续学习

一句话总结

单元文件中最重要的一行是 Type=,因为 systemd 依靠它判断“这个服务何时算启动完成”。

概念图: Type= · 不要直接修改软件包提供的单元。 · After 与 Requires 不同。 · 顺序

为什么需要了解这些

systemctl start myapp 卡住 30 秒后失败,但进程明明已经正常启动。这种情况通常由 Type 引起。

工作原理

单元文件的位置与优先级

路径 优先级 用途
/etc/systemd/system/ 管理员自定义单元(覆盖配置)
/run/systemd/system/ 运行时生成
/usr/lib/systemd/system/ 软件包安装的默认单元

不要直接修改软件包提供的单元。 更新时修改会被覆盖。标准做法是使用 systemctl edit <유닛> 创建 /etc/systemd/system/<유닛>.d/override.conf

三个区段

[Unit]
Description=My Web Application
Documentation=https://wiki.internal/myapp
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
ConditionPathExists=/etc/myapp/config.yaml

[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=-/etc/myapp/env
ExecStartPre=/opt/myapp/bin/check-config --validate
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
TimeoutStartSec=30
StandardOutput=journal
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

AfterRequires 不同。 After 只规定顺序Requires 则建立依赖关系。如果只设置依赖而没有顺序,两个服务可能同时启动。通常应同时使用二者。

EnvironmentFile=- 中的连字符表示“即使文件不存在也不视为失败”。

Type 对比

Type 判断启动完成的时机 适用服务
simple(默认) ExecStart 进程一启动 前台守护进程
exec 成功 exec() 二进制文件时 比 simple 更准确
forking ExecStart 退出且子进程仍存活时 传统 fork 守护进程,需要 PIDFile=
oneshot ExecStart 完全退出时 初始化脚本
notify 服务发送 sd_notify(READY=1) 能主动报告就绪状态的服务
dbus 注册 D-Bus 名称时 需要 BusName=

最常见的错误是设置了 Type=simple,进程却会守护进程化(fork 后父进程退出)。 systemd 会把父进程退出视为服务终止并判定失败。反过来,如果设置 Type=forking,程序却以前台方式运行,systemd 会因为父进程迟迟不退出而永远等待,最终触发 TimeoutStartSec 并将其终止。这就是“启动卡住 30 秒后失败”的典型原因。

生产环境推荐使用 Type=notify 服务可以准确报告真正具备请求处理能力的时刻,从而让依赖服务安全启动。

五种常见错误

  1. 在 ExecStart 中使用 shell 功能。 管道、重定向、变量展开、通配符都不会生效,因为 systemd 不通过 shell,而是直接执行程序。确有需要时,用 ExecStart=/bin/bash -c '...' 包裹命令。
  2. 使用相对路径。 ExecStart=myapp 会失败,必须使用绝对路径。
  3. 缺少 [Install] 此时 systemctl enable 不会执行任何操作,服务也无法在开机时自动启动。
  4. Restart=always + RestartSec=0 崩溃循环会拖垮系统。应通过 StartLimitIntervalSec/StartLimitBurst 设置上限。
  5. 使用 Type=forking 却没有 PIDFile= systemd 无法找到主进程,导致跟踪错乱。

安全加固

NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/myapp /var/log/myapp

ProtectSystem=strict 会将 /usr/boot/etc 设为只读。需要写入的路径通过 ReadWritePaths 设为例外。启用后服务无法启动,通常是因为遗漏了某个写入路径。

定时器

它可以替代 cron,由 .timer.service 配对使用。

# labhub-backup.timer
[Unit]
Description=Nightly backup

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
Unit=labhub-backup.service

[Install]
WantedBy=timers.target

Persistent=true 表示:如果系统关机期间错过了执行时间,开机后立即补执行。这是 cron 不具备的功能。

生产现场中的常见情况

可以使用 systemd-analyze verify <유닛> 检查语法和引用。在发布前执行这项检查是个好习惯。

下一次实验要做什么

从头编写单元文件,创建配套的定时器,并亲自制作单元验证脚本。评分器会分别使用正常单元和错误单元来运行它。