Go expvar 和运行时指标:把 GC 偶顿和 goroutine 数接入监控
Go expvar 和运行时指标:把 GC 偶顿和 goroutine 数接入监控
一、背景与问题
Go 服务的运行时状态直接影响稳定性,但大多数人只看业务指标(QPS、延迟、错误率),忽略了 GC 停顿时间飙升、goroutine 泄漏、内存分配异常这些底层信号。等到服务 OOM 或响应卡顿才发现问题,已经晚了。
runtime/metrics和expvar是 Go 标准库提供的运行时指标来源。它们不是额外依赖,不是第三方包,是每个 Go 进程自带的数据出口。问题在于:这些数据默认只在/debug/vars这个 JSON 端点上裸露,没有时间序列、没有告警阈值、没有和业务指标的关联。把它们接入 Prometheus 体系,是成本最低、收益最高的运行时可观测性建设。
二、指标体系梳理
Go 运行时指标不是"能暴露多少暴露多少",而是按诊断场景筛选。
graph TD A[Go Runtime Metrics] --> B[GC 指标组] A --> C[Goroutine 指标组] A --> D[Memory 指标组] A --> E[Scheduler 指标组] B --> B1[gc_pause_total_ns] B --> B2[gc_cycles] B --> B3[gc_heap_allocs_bytes] C --> C1[goroutine_count] C --> C2[goroutine_live_count] D --> D1[heap_alloc_bytes] D --> D2[heap_sys_bytes] D --> D3[stack_inuse_bytes] E --> E1[scheduler_goroutines_runnable] E --> E2[scheduler_time_slice_total_ns] style A fill:#1a237e,color:#fff style B fill:#283593,color:#fff style C fill:#303f9f,color:#fff style D fill:#3949ab,color:#fff style E fill:#3f51b5,color:#fff四组指标对应四个诊断场景:
| 指标组 | 诊断场景 | 核心指标 |
|---|---|---|
| GC | 延迟突增排查 | gc_pause_total_ns, gc_cycles |
| Goroutine | 资源泄漏排查 | goroutine_count |
| Memory | OOM 前预警 | heap_alloc_bytes / heap_sys_bytes |
| Scheduler | 调度瓶颈排查 | runnable goroutine 数 |
Go 1.17+ 推荐使用runtime/metrics替代旧的runtime.ReadMemStats。原因:ReadMemStats会触发 STW(Stop-The-World),在采集指标的同时制造 GC 停顿——这本身就是你要监控的问题。runtime/metrics基于采样,不会干扰运行时行为。
三、实现方案
3.1 expvar 到 Prometheus 的桥接
Go 标准库expvar默认在/debug/vars输出 JSON。要接入 Prometheus,需要一个桥接器把 expvar 数据转换为 Prometheus metric format。
package metrics import ( "runtime/metrics" "expvar" ) var ( goroutineGauge = promauto.NewGauge(prometheus.GaugeOpts{ Name: "go_goroutines_current", }) gcPauseGauge = promauto.NewGauge(prometheus.GaugeOpts{ Name: "go_gc_pause_seconds_total", }) heapAllocGauge = promauto.NewGauge(prometheus.GaugeOpts{ Name: "go_heap_alloc_bytes", }) ) func CollectRuntimeMetrics() { // 使用 runtime/metrics API samples := []metrics.Sample{ {Name: "/sched/goroutines:goroutines"}, {Name: "/gc/pause-total:nanoseconds"}, {Name: "/memory/classes/heap/objects:bytes"}, } metrics.Read(samples) for _, s := range samples { switch s.Name { case "/sched/goroutines:goroutines": goroutineGauge.Set(float64(s.Value.Uint64())) case "/gc/pause-total:nanoseconds": gcPauseGauge.Set(float64(s.Value.Uint64()) / 1e9) case "/memory/classes/heap/objects:bytes": heapAllocGauge.Set(float64(s.Value.Uint64())) } } }这段代码用runtime/metrics采集数据,写入 Prometheus Gauge。在main中启动定时采集:
go func() { for range time.Tick(5 * time.Second) { CollectRuntimeMetrics() } }()5 秒采集间隔足够。GC 停顿是微秒级事件,5 秒汇总不影响诊断精度;goroutine 数变化通常是渐进的,5 秒粒度完全够用。
3.2 自定义 expvar 指标
标准库expvar默认只暴露memstats和cmdline。你需要注册业务相关的运行时指标:
var inferenceQueueDepth = expvar.NewInt("inference_queue_depth") func (s *Service) ProcessRequest(ctx context.Context, req *Request) error { inferenceQueueDepth.Add(1) defer inferenceQueueDepth.Add(-1) // ... }然后在桥接器中把它也接入 Prometheus:
queueGauge := promauto.NewGaugeFunc(prometheus.GaugeOpts{ Name: "inference_queue_depth_current", }, func() float64 { val := expvar.Get("inference_queue_depth") if val == nil { return 0 } return float64(val.(*expvar.Int).Value()) })这样推理队列深度和 GC 停顿在同一套 Prometheus 体系下可交叉查询——当推理延迟飙升时,你可以同时看到 GC 停顿是否也飙升,快速判断是 GC 问题还是业务排队问题。
四、告警规则与生产实践
4.1 关键告警规则
| 告警 | 条件 | 级别 |
|---|---|---|
| Goroutine 泄漏 | goroutine_count > 5000 持续 5m | P1 |
| GC 停顿异常 | gc_pause_seconds_total 增加 > 0.1/s 持续 3m | P2 |
| 内存逼近上限 | heap_alloc_bytes > 80% heap_sys_bytes | P2 |
| Runnable 堆积 | scheduler_goroutines_runnable > 100 持续 2m | P3 |
goroutine 泄漏的阈值不能一刀切。高并发推理服务正常运行可能就有几千 goroutine(每个推理请求一个),关键是看趋势而非绝对值。告警条件加上"持续 5 分钟"的窗口,过滤掉正常的瞬时波动。
4.2 GC 停顿和业务延迟的关联分析
在 Grafana 中,把 GC 停顿和请求延迟叠在同一面板:
# GC 停顿时间(5s 窗口内的增量) rate(go_gc_pause_seconds_total[5s]) # 请求 P99 延迟 histogram_quantile(0.99, rate(http_request_duration_seconds_bucket[5m]))当两条曲线同步波动时,延迟问题根因是 GC;当 GC 平稳但延迟飙升时,问题在业务层或下游。这个关联判断是运行时指标接入监控的核心收益——不是多看几个数字,是建立因果链。
4.3 踩坑记录
expvar.Publish的并发安全。expvar.Int和expvar.Map内部有互斥锁,并发读写没问题。但如果你自定义了expvar.Func返回复杂结构,要确保函数内部不访问共享状态——Func会在 HTTP handler 中被调用,并发度很高。
runtime/metrics的指标名称格式。指标名用/分隔命名空间,如/gc/pause-total:nanoseconds。冒号后面的后缀表示值的单位。在转换为 Prometheus 指标时,必须按单位换算:nanoseconds→seconds,bytes 保持不变。这个换算如果出错,GC 停顿会显示成几百万秒。
不要采集所有指标。runtime/metrics定义了 50+ 个指标。全部采集会增加 Collector 的存储压力,更会让 dashboard 变成噪声面板。只采集前面四组的核心指标,其余在调试时用curl /debug/vars手动查看。
五、总结
Go 运行时指标是服务自带的诊断信号源,零依赖、零侵入。把它们接入 Prometheus 不是锦上添花,是基础设施的基本职责。
三个关键实践:
- 用
runtime/metrics替代ReadMemStats:后者会触发 STW,用你要监控的手段制造你要监控的问题,这是矛盾。 - 按诊断场景筛选指标:GC、Goroutine、Memory、Scheduler 四组,每组只取核心指标,其余按需手动查看。
- 运行时指标和业务指标必须同体系:只有同一套 Prometheus 下才能做关联分析,GC 停顿和请求延迟叠图是定位问题最快的手段。
基础设施不需要漂亮话。运行时指标的价值不在于 dashoard 有多少面板,在于 OOM 发生前能不能提前 30 分钟看到 heap_alloc 曲线逼近上限。能提前看到,就能提前处理;看不到,就只能事后复盘。
