把 systemd unit 文件写准
一句话总结
单元文件中最重要的一行是 Type=,因为 systemd 依靠它判断“这个服务何时算启动完成”。
为什么需要了解这些
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
After 与 Requires 不同。 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。 服务可以准确报告真正具备请求处理能力的时刻,从而让依赖服务安全启动。
五种常见错误
- 在 ExecStart 中使用 shell 功能。 管道、重定向、变量展开、通配符都不会生效,因为 systemd 不通过 shell,而是直接执行程序。确有需要时,用
ExecStart=/bin/bash -c '...'包裹命令。 - 使用相对路径。
ExecStart=myapp会失败,必须使用绝对路径。 - 缺少
[Install]。 此时systemctl enable不会执行任何操作,服务也无法在开机时自动启动。 Restart=always+RestartSec=0。 崩溃循环会拖垮系统。应通过StartLimitIntervalSec/StartLimitBurst设置上限。- 使用
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 <유닛> 检查语法和引用。在发布前执行这项检查是个好习惯。
下一次实验要做什么
从头编写单元文件,创建配套的定时器,并亲自制作单元验证脚本。评分器会分别使用正常单元和错误单元来运行它。