陈默的笔记

用 systemd 管一个 Go 服务

· Go / systemd

前天把一个内部小服务的部署方式从「nohup + 手写重启脚本」换成了 systemd。本来以为十分钟的事,结果在几个细节上折腾了一个多小时,记一下。

unit 文件

[Unit]
Description=metrics-collector
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=metrics
WorkingDirectory=/opt/metrics
EnvironmentFile=-/etc/metrics/env
ExecStart=/opt/metrics/collector -config /etc/metrics/config.yaml
Restart=on-failure
RestartSec=3
LimitNOFILE=65535

[Install]
WantedBy=multi-user.target

第一个坑:EnvironmentFile 里的引号

EnvironmentFile 不是 shell,它不处理引号转义。我写了 DB_PASS="p@ss word",结果引号被当成值的一部分,连密码都带上双引号了,连库失败还没报错——因为密码错和连接拒绝在日志里长得差不多。

正确写法是不要引号,空格直接写:

DB_PASS=p@ss word

第二个坑:WorkingDirectory 和相对路径

原来 nohup 是在项目目录里跑的,代码里有个 ./data 的相对路径。换成 systemd 之后 WorkingDirectory 没配,它就跑到 / 下面去建目录了,权限不够直接崩。

结论:服务里能用绝对路径就别用相对的,或者至少把 WorkingDirectory 显式写死。

Restart=always 还是 on-failure

我一直用 on-failure。原因是 always 会在正常退出(比如收到 SIGTERM 做优雅关闭)后也重启,配合 systemctl stop 会出现「停不下去」的假象。

需要常驻的用 on-failure,需要「无论如何都活着」的才用 always,而且最好加 StartLimitIntervalSec 防止疯狂重启。

日志

直接输出到 stdout,交给 journald 就行:

journalctl -u metrics-collector -f
journalctl -u metrics-collector --since "1 hour ago"

比自己切文件省事。真需要长期留存,再配 SystemMaxUse 或者转发到别处。

优雅重启

Go 这边只要监听 SIGTERM 并调 http.Server.Shutdown,systemd 默认就会发 SIGTERM:

srv := &http.Server{Addr: ":8080", Handler: mux}
go srv.ListenAndServe()

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
<-sig

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)

注意 Timeout 要小于 systemd 的 TimeoutStopSec(默认 90 秒),不然会被 SIGKILL,正在处理的请求就断了。

改完到现在跑了两个多月,没再出现过「进程还在但端口没起来」的怪状态。早该换了。