应使用 gaugevec 按连接生命周期精准打点,而非依赖 go_goroutines 或 runtime.numgoroutine();需显式注册、收敛标签、异步更新,并确保每次建连 inc、断连 dec 严格匹配连接状态。

直接用 go_goroutines 指标看长连接协程数是无效的——它混着 netpoll、timer、sysmon 等 runtime 内部协程,也掺着 DB 连接池、日志 flusher 等非连接 goroutine,数值跳变根本无法区分是正常建连还是泄漏。
为什么 runtime.NumGoroutine() 不能替代连接级打点
它返回的是当前所有活跃 goroutine 总数(含阻塞中),开销低、原子读取,适合做全局水位参考,但不具备连接维度信息:
- 数值稳定在 5–10 不代表没泄漏:可能有 3 个 goroutine 卡在
conn.Read()或上,它们仍算“活跃”,但业务已停滞 - 压测时从 12 → 846,其中可能只有 500 是 WebSocket handler,其余是临时任务或 channel receive 协程
- 滚动发布后曲线阶梯上升,大概率是多个 pod 的
go_goroutines被 Prometheus 合并抓取,而非真实增长 - 客户端断开后数值不降?查
pprof/goroutine?debug=2,八成是 close handler 没触发或sync.Once漏写,导致 goroutine 卡在 channel receive
必须用 GaugeVec 按连接生命周期打点
长连接的建立、活跃、异常关闭、超时释放,必须映射为可增可减的瞬时值。CounterVec 只能递增,不适用;GaugeVec 才能 .Inc() 建连、.Dec() 断连:
- 标签设计要收敛:
protocol("ws"/"grpc")、status("active"/"closed")、path(如 "/stream/user"),禁用 client_id 等动态值,否则 series 爆炸 - 注册必须显式绑定私有注册器:
reg := prometheus.NewRegistry(),再reg.MustRegister(wsConnGauge),绝不用prometheus.DefaultRegisterer - 初始化一次,全局复用:
var wsConnGauge = prometheus.NewGaugeVec(...)放包级变量,不在 handler 里反复 new - 打点位置要精准:WebSocket 在
conn.SetCloseHandler和conn.Close()后调用.Dec();gRPC streaming 在stream.Context().Done()触发时.Dec(),且加sync.Once防重复扣减
GaugeVec 更新必须线程安全且异步
长连接 goroutine 往往在不同 goroutine 中创建/销毁(比如 HTTP handler 启一个,signal handler 清一个),直接 .Inc()/.Dec() 会触发 GaugeVec 内部 mutex 竞争,高频建连断连下 /metrics 渲染延迟飙升:
- 不要在 handler 入口直接
wsConnGauge.WithLabelValues("ws", "active", "/chat").Inc() - 改用带锁队列或
chan struct{}异步通知后台 goroutine 统一更新 - 或者用
sync/atomic维护本地计数器,再定期批量同步到GaugeVec,降低锁竞争频次 - 避免把
GaugeVec注册到默认注册器后又在多个模块里各自.WithLabelValues(...).Inc()—— 标签组合不一致会导致指标分裂,查询时sum by (status)失效
真正难的不是采集数字,而是让每个 .Inc() 和 .Dec() 都落在连接生命周期的精确节点上;漏掉一次 .Dec(),监控曲线就永远高估,而你很难从 Prometheus 图表里看出哪一行少了一次扣减。











