systemd 服务管理实战:从 unit 文件、资源限制到看门狗的完整链路 原创

温馨提示:
本文最后更新于 2026-10-22,已超过 0 天没有更新。 若文章内的图片失效(无法正常加载),请留言反馈或直接 联系我。

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 就完事。