systemd 教程 / Service Unit 详解
Service Unit 详解
一、[Service] 段核心参数
1.1 启动参数
| 参数 | 说明 | 示例 |
|---|---|---|
Type | 服务进程类型 | Type=simple |
ExecStart | 启动命令 | ExecStart=/usr/bin/nginx |
ExecStartPre | 启动前执行 | ExecStartPre=/usr/sbin/nginx -t |
ExecStartPost | 启动后执行 | ExecStartPost=/bin/sleep 1 |
ExecReload | 重载命令 | ExecReload=/bin/kill -HUP $MAINPID |
ExecStop | 停止命令 | ExecStop=/bin/kill -QUIT $MAINPID |
ExecStopPost | 停止后执行 | ExecStopPost=/usr/bin/cleanup.sh |
RemainAfterExit | 进程退出后保持 active | RemainAfterExit=yes |
完整示例:
[Unit]
Description=My Web Application
After=network.target postgresql.service
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/myapp
Environment=NODE_ENV=production
EnvironmentFile=/etc/myapp/env
ExecStartPre=/opt/myapp/scripts/pre-start.sh
ExecStart=/usr/bin/node /opt/myapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID
Restart=on-failure
RestartSec=5
TimeoutStartSec=30
TimeoutStopSec=30
[Install]
WantedBy=multi-user.target
二、Type 类型详解
| Type | 说明 | 适用场景 |
|---|---|---|
simple | ExecStart 进程即主进程(默认) | 前台运行的程序 |
forking | 进程 fork 到后台 | 传统 daemon(如 Nginx daemon 模式) |
oneshot | 执行完毕后退出 | 初始化脚本、一次性任务 |
dbus | 获取 D-Bus 名称后就绪 | D-Bus 服务 |
notify | 发送 sd_notify() 后就绪 | 支持 notify 协议的服务 |
idle | 等待其他任务完成后启动 | 控制台输出服务 |
2.1 simple(最常用)
[Service]
Type=simple
ExecStart=/usr/bin/myapp --foreground
💡 提示:如果程序在前台运行,就用 simple。这是最常见的类型。
2.2 forking
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStart=/usr/sbin/nginx
⚠️ 注意:forking 类型强烈建议设置 PIDFile,否则 systemd 可能无法正确追踪主进程。
2.3 oneshot
[Service]
Type=oneshot
ExecStart=/usr/local/bin/initialize-db.sh
ExecStart=/usr/local/bin/seed-data.sh
RemainAfterExit=yes
oneshot 可以指定多个 ExecStart,按顺序执行。RemainAfterExit=yes 使任务完成后 Unit 仍保持 active。
2.4 notify
[Service]
Type=notify
ExecStart=/usr/bin/myapp
NotifyAccess=main
服务就绪时调用 sd_notify(0, "READY=1"),适合启动时间较长的服务:
// C 示例
#include <systemd/sd-daemon.h>
int main() {
init_database();
load_config();
sd_notify(0, "READY=1"); // 通知 systemd 已就绪
// 主循环...
}
三、Restart 策略
3.1 Restart 参数值
| 值 | 触发条件 |
|---|---|
no | 不自动重启(默认) |
on-success | 退出码为 0 |
on-failure | 非零退出码、被信号杀死、超时、看门狗 |
on-abnormal | 被信号杀死、超时、看门狗 |
on-abort | 收到未捕获的信号 |
on-watchdog | 看门狗超时 |
always | 无论何种原因都重启 |
3.2 Restart 与退出方式对照
| Restart 值 | 正常退出(0) | 异常退出(非0) | 被信号杀死 | 超时 |
|---|---|---|---|---|
no | ❌ | ❌ | ❌ | ❌ |
on-success | ✅ | ❌ | ❌ | ❌ |
on-failure | ❌ | ✅ | ✅ | ✅ |
on-abnormal | ❌ | ❌ | ✅ | ✅ |
always | ✅ | ✅ | ✅ | ✅ |
3.3 重启频率限制
[Service]
Restart=on-failure
RestartSec=5
StartLimitBurst=3 # 10 秒内最多重启 3 次
StartLimitIntervalSec=10
| 参数 | 说明 | 默认值 |
|---|---|---|
RestartSec | 重启前等待时间 | 100ms |
StartLimitBurst | 时间窗口内最大重启次数 | 5 |
StartLimitIntervalSec | 时间窗口 | 10s |
💡 提示:合理的 RestartSec(建议 5-10 秒)可避免服务崩溃时的 CPU 风暴。
四、环境变量
[Service]
# 行内设置
Environment=MY_VAR1=value1 MY_VAR2=value2
Environment="LANG=en_US.UTF-8"
# 外部文件(- 前缀表示文件不存在时不报错)
EnvironmentFile=/etc/myapp/env
EnvironmentFile=-/etc/myapp/env.local
环境文件格式(/etc/myapp/env):
# 注释行
DATABASE_URL=postgresql://user:pass@localhost/myapp
REDIS_URL=redis://localhost:6379
LOG_LEVEL=info
⚠️ 注意:- 前缀表示文件不存在时不报错。环境变量优先级:命令行变量 > EnvironmentFile > Environment。
五、用户与权限
[Service]
User=www-data
Group=www-data
SupplementaryGroups=www-data docker
WorkingDirectory=/opt/myapp
UMask=0027
六、资源限制
6.1 传统限制(ulimit)
[Service]
LimitNOFILE=65535 # 最大打开文件描述符
LimitNPROC=4096 # 最大进程数
LimitMEMLOCK=infinity # 最大锁定内存
LimitCORE=infinity # 核心转储文件大小
6.2 CGroup 资源控制(systemd v232+)
[Service]
MemoryMax=2G # 硬内存限制
MemoryHigh=1G # 软限制(超过后节流)
CPUQuota=200% # CPU 配额(200% = 2 核)
CPUWeight=100 # CPU 权重
TasksMax=512 # 最大任务数
| 参数 | 说明 | 示例 |
|---|---|---|
MemoryMax | 硬内存限制 | 2G、512M |
MemoryHigh | 软限制(超过后 swap 节流) | 1G |
CPUQuota | CPU 配额百分比 | 200%(2 核) |
CPUWeight | CPU 权重(10-10000) | 100 |
TasksMax | 最大任务数 | 512 |
七、安全加固
[Service]
# 文件系统保护
ProtectSystem=strict # /usr 只读
ProtectHome=yes # /home 不可见
PrivateTmp=yes # 临时目录私有化
ReadWritePaths=/var/lib/myapp # 可写路径(配合 strict)
PrivateDevices=yes # 设备访问私有化
# 权限控制
NoNewPrivileges=yes # 禁止提权
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
# 系统保护
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
💡 提示:使用 systemd-analyze security myapp.service 检查安全评分(0-10,越低越安全)。
八、PID 文件与看门狗
8.1 PID 文件
[Service]
Type=forking
PIDFile=/run/myapp.pid
ExecStart=/usr/bin/myapp --daemon
⚠️ 注意:PID 文件必须在主进程 fork 完成后由主进程自己写入。
8.2 看门狗(Watchdog)
[Service]
Type=notify
WatchdogSec=30
服务必须定期发送心跳(sd_notify(0, "WATCHDOG=1")),超时未收到则标记失败。
⚠️ 注意:需要程序代码中集成 sd_notify() 调用,没有代码支持时不要启用。
九、生产场景
场景 1:Node.js 应用服务
[Unit]
Description=Node.js API Server
After=network.target redis.service
Requires=redis.service
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/api-server
EnvironmentFile=/etc/api-server/env
Environment=NODE_ENV=production
ExecStart=/usr/bin/node --max-old-space-size=2048 /opt/api-server/dist/server.js
Restart=on-failure
RestartSec=5
StartLimitBurst=3
StartLimitIntervalSec=30
MemoryMax=3G
CPUQuota=200%
LimitNOFILE=65535
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/var/log/api-server /var/lib/api-server
[Install]
WantedBy=multi-user.target
场景 2:一次性备份脚本
[Unit]
Description=Database Backup
After=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup-db.sh
TimeoutStartSec=3600
StandardOutput=journal
StandardError=journal
SyslogIdentifier=db-backup