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_cyclesGoroutine资源泄漏排查goroutine_countMemoryOOM 前预警heap_alloc_bytes / heap_sys_bytesScheduler调度瓶颈排查runnable goroutine 数Go 1.17 推荐使用runtime/metrics替代旧的runtime.ReadMemStats。原因ReadMemStats会触发 STWStop-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) // ... }然后在桥接器中把它也接入 PrometheusqueueGauge : 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 持续 5mP1GC 停顿异常gc_pause_seconds_total 增加 0.1/s 持续 3mP2内存逼近上限heap_alloc_bytes 80% heap_sys_bytesP2Runnable 堆积scheduler_goroutines_runnable 100 持续 2mP3goroutine 泄漏的阈值不能一刀切。高并发推理服务正常运行可能就有几千 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→secondsbytes 保持不变。这个换算如果出错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 曲线逼近上限。能提前看到就能提前处理看不到就只能事后复盘。
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_cyclesGoroutine资源泄漏排查goroutine_countMemoryOOM 前预警heap_alloc_bytes / heap_sys_bytesScheduler调度瓶颈排查runnable goroutine 数Go 1.17 推荐使用runtime/metrics替代旧的runtime.ReadMemStats。原因ReadMemStats会触发 STWStop-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) // ... }然后在桥接器中把它也接入 PrometheusqueueGauge : 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 持续 5mP1GC 停顿异常gc_pause_seconds_total 增加 0.1/s 持续 3mP2内存逼近上限heap_alloc_bytes 80% heap_sys_bytesP2Runnable 堆积scheduler_goroutines_runnable 100 持续 2mP3goroutine 泄漏的阈值不能一刀切。高并发推理服务正常运行可能就有几千 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→secondsbytes 保持不变。这个换算如果出错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 曲线逼近上限。能提前看到就能提前处理看不到就只能事后复盘。