systemd 服务管理实战:从 unit 文件、资源限制到看门狗的完整链路 原创
systemd 不只是「开机启动服务」——它是一套完整的进程管理系统:依赖编排、资源隔离、日志聚合、自动重启、看门狗。大部分运维只用到了 systemctl start/stop,真正有价值的特性(cgroup 限制、journald 日志、sd_notify)反而被忽略。这篇把生产环境真正用到的 systemd 能力串起来。
一、一个生产级 unit 文件长什么样
以一个 Go Web 服务为例,覆盖 90% 的生产需求:
# /etc/systemd/system/myapp.service
[Unit]
Description=MyApp Web Service
Documentation=https://wiki.example.com/myapp
After=network-online.target mysql.service redis.service
Wants=network-online.target
# 依赖服务挂了自己也挂(可选)
# Requires=mysql.service
[Service]
Type=notify # 支持 sd_notify 的就绪信号
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
StartLimitIntervalSec=300
StartLimitBurst=5
# 运行身份
User=myapp
Group=myapp
# 资源限制(cgroup v2)
MemoryMax=512M
MemoryHigh=400M
CPUQuota=150%
TasksMax=200
LimitNOFILE=65535
# 安全加固
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/myapp /var/log/myapp
PrivateTmp=true
# 环境
Environment=GOGC=100
EnvironmentFile=-/etc/myapp/env
# 日志
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
二、Type 的选择:别再写 Type=simple 了
| Type | 就绪判定 | 适用场景 |
|---|---|---|
| simple | fork 立即就绪 | 快速启动的脚本 |
| exec | execve 成功即就绪 | 比 simple 更准 |
| forking | 主进程退出即就绪 | 传统 daemon |
| notify | 进程主动发 sd_notify | 推荐,支持慢启动 |
| oneshot | 命令执行完 | 一次性任务 |
Type=notify 的价值在于:服务真正「准备好接收流量」时才标记为 active。配合 After= 依赖,确保下游服务拿到的是「真正可用」的上游,而不是「刚 fork 完还在加载配置」的上游。
Go 里集成只需一个包:
import "github.com/coreos/go-systemd/v22/daemon"
// 服务就绪后
daemon.SdNotify(false, daemon.SdNotifyReady)
三、Restart 策略:别再用 always
Restart=on-failure # 推荐:只有非零退出码才重启
RestartSec=5s # 每次重启间隔
StartLimitBurst=5 # 300 秒内最多重启 5 次
StartLimitIntervalSec=300
always 的问题:服务正常退出(比如收到 SIGTERM 主动关闭)也会重启,掩盖了真正的停机意图。on-failure 只在崩溃、OOM 被杀、看门狗超时时才拉起。
超过 StartLimitBurst 后服务进入 failed 状态,这时应该触发告警——systemd 本身不发告警,需要外部监控(Prometheus node_exporter 的 node_systemd_unit_state 指标)。
四、资源限制:cgroup v2 的正确姿势
MemoryMax=512M # 硬上限,超了 OOM-kill
MemoryHigh=400M # 软上限,开始回收
CPUQuota=150% # 最多用 1.5 个核
TasksMax=200 # 最大线程数
LimitNOFILE=65535 # 文件句柄
查看当前限制是否生效:
systemctl show myapp | grep -E 'Memory|CPU|Tasks'
cat /sys/fs/cgroup/system.slice/myapp.service/memory.max
五、日志:journald 的正确用法
# 看最近 100 行
journalctl -u myapp -n 100
# 跟踪
journalctl -u myapp -f
# 看上次启动到现在的
journalctl -u myapp -b
# 按优先级过滤(只看 error 及以上)
journalctl -u myapp -p err
# 看 OOM 被杀的记录
journalctl -u myapp | grep -i 'oom\|killed'
# 磁盘占用
journalctl --disk-usage
默认 journald 会持久化到 /var/log/journal/,重启不丢。长期存储建议配 SystemMaxUse=1G 防止撑爆磁盘。
六、看门狗:让 systemd 监控你的应用健康
[Service]
WatchdogSec=30s
应用每 30 秒内向 systemd 发一次心跳(sd_notify("WATCHDOG=1")),超时未发则 systemd 判定应用卡死,强制重启。比外部健康检查更可靠——能检测到「进程还在但死锁」的情况。
七、实战:排查「服务被 OOM 杀了但不知道为什么」
# 1. 确认是 OOM
journalctl -u myapp | grep -i 'oom\|out of memory\|killed'
# 2. 看内存限制
systemctl show myapp | grep Memory
# 3. 看 cgroup 里的历史峰值
cat /sys/fs/cgroup/system.slice/myapp.service/memory.peak
# 4. 临时调大验证
systemctl edit myapp
# 加:
# [Service]
# MemoryMax=1G
systemctl restart myapp
# 5. 长期方案:加 MemoryHigh 让内核提前回收,避免硬 OOM
结语
systemd 的价值不在「替代 init」,而在把进程管理从脚本化变成了声明式。unit 文件里声明依赖、资源、重启策略,systemd 负责执行和监控。配合 journald 和 cgroup,它提供了比 supervisor、pm2 更底层、更稳定的进程管理能力——关键是你得用到 notify、Watchdog、MemoryMax 这些真正有用的特性,而不是只写个 ExecStart 就完事。