用 systemd 管理 go 进程最可靠:type=simple、restart=always、非 root 用户运行;健康检查走 http 注册/注销;进程监听用 netlink/kqueue 事件驱动;监控用 prometheus/client_golang 暴露标准指标;日志用 zap/zerolog 结构化,与指标解耦。

用 systemd 管理 Go 进程是最小侵入、最可靠的选择
别写 daemon 化逻辑,也别在业务代码里塞 exec.Command 启子进程——Go 二进制本身已是完整可执行单元,交给 systemd 就行。它天然支持崩溃自动重启、依赖声明(比如等 consul.service 就绪后再启)、资源限制(LimitNOFILE)、日志聚合(journalctl -u user-service),且无需额外进程或 SDK。
关键点:Type=simple(不是 forking),Restart=always,After=network.target consul.service。避免把健康检查写成 shell 脚本调用,直接在 Go 服务启动后发 HTTP 注册请求,退出前主动注销。
- Unit 文件必须放在
/etc/systemd/system/下,改完要systemctl daemon-reload -
User=和Group=必须是普通用户,禁止用root运行业务进程 -
WorkingDirectory和ExecStart路径需绝对,不能用~或相对路径
进程存活监听别轮询,用 netlink proc connector(Linux)或 kqueue(FreeBSD)
想监听某个非子进程(比如另一个微服务)是否退出?kill -0 pid 轮询是错的——CPU 飙高不说,还存在 PID 复用竞态:目标进程刚死,新进程立刻占了同一 PID,你误判为“还在运行”。
正确做法是事件驱动:
- Linux 下用
netlink的NETLINK_CONNECTOR+PROC_EVENT_EXIT,内核级通知,零 CPU 开销,无需 root 权限 - FreeBSD 下用
kqueue+EVFILT_PROC+NOTE_EXIT,同样精准低开销 - 跨平台抽象库如
github.com/moby/sys/pidfile或gopsutil的process.NewProcess(pid).Wait()本质仍是轮询,慎用
指标暴露优先走 Prometheus,别自己拼 HTTP handler
Go 应用监控不是“打印日志+看 tail”,而是暴露结构化指标供拉取。用 prometheus/client_golang 是事实标准,不是可选项。
常见错误是手写 http.HandleFunc("/metrics", ...) 拼字符串——格式错一个空格就让 Prometheus 抓不到;或者每秒都算一次 CPU 百分比并塞进指标,导致采集卡顿。
- 只暴露
Counter、Gauge、Histogram这三类原语,不要暴露原始时间序列数据 - HTTP handler 必须注册到
promhttp.Handler(),不是自己 render 文本 - 业务逻辑里调用
httpRequests.WithLabelValues(r.Method, r.URL.Path, strconv.Itoa(status)).Inc(),别在 handler 里做耗时计算 - 避免在 goroutine 里反复
prometheus.MustRegister(),注册一次即可
日志必须结构化,且与指标解耦
用 zap 或 zerolog,不是 log.Printf。结构化日志能被 Loki、ELK 直接索引,而 printf 日志只能 grep。
但日志和指标不能混用:日志记录“发生了什么”(例如 {"event":"user_deleted","user_id":123,"ip":"10.0.1.5"}),指标记录“发生了多少次”(user_delete_total{region="sh"} 42)。强行用日志打计数器,会导致日志量爆炸、查询变慢、存储成本飙升。
- 日志字段名统一小写+下划线(
request_id,不是requestId),方便下游解析 - 敏感字段如
password、token必须显式过滤,zap 提供zap.String("token", redact(token))模式 - 不要在日志里打 stack trace 全量,用
logger.Error("failed to process order", zap.Error(err))让 zap 自动提取
RestartSec 和 Prometheus 的 scrape interval 容易冲突——比如服务刚崩,systemd 10 秒后重启,但 Prometheus 每 15 秒拉一次,中间有 5 秒空白窗口。这需要靠 up{job="user-service"} == 0 告警来补位,而不是指望单个组件全覆盖。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











