bigywater.tech
Go · 65

runtime/metrics:读运行时指标的稳定接口

runtime.ReadMemStats 能读的东西有限,而且读一次要 2 万多纳秒。runtime/metrics 是更稳定的替代:每个指标一个字符串键,可以只读你要的。这篇列出全部指标,和 MemStats 对账,量读取成本,并读了一个调度等待的直方图。

107
Go 1.27.1 里 metrics.All() 返回的指标数
188 ns vs 22760 ns
读 1 个指标与 runtime.ReadMemStats 的单次耗时
66 vs 51
/sched/goroutines 指标与 NumGoroutine 的差:前者包含系统 goroutine

指标清单

实测 · 本机执行输出
  metrics.All() 共 107 个指标;累计型(Cumulative)75 个;按取值类型:uint64 87 / float64 12 / 直方图 8
  按第一段路径分组:/cgo/…:1  /cpu/…:11  /gc/…:26  /godebug/…:41  /memory/…:14  /sched/…:13  /sync/…:1
    /gc/heap/live:bytes                          uint64  累计=false
    /memory/classes/total:bytes                  uint64  累计=false
    /sched/gomaxprocs:threads                    uint64  累计=false
    /sched/goroutines-created:goroutines         uint64  累计=true
    /sched/goroutines/not-in-go:goroutines       uint64  累计=false
    /sched/goroutines/runnable:goroutines        uint64  累计=false
    /sched/goroutines/running:goroutines         uint64  累计=false
    /sched/goroutines/waiting:goroutines         uint64  累计=false
    /sched/goroutines:goroutines                 uint64  累计=false
    /sched/latencies:seconds                     直方图     累计=true
    /sched/pauses/stopping/gc:seconds            直方图     累计=true
    /sched/pauses/stopping/other:seconds         直方图     累计=true
    /sched/pauses/total/gc:seconds               直方图     累计=true
    /sched/pauses/total/other:seconds            直方图     累计=true
    /sched/threads/total:threads                 uint64  累计=false

107 个指标里 /godebug 占 41 个(每个 GODEBUG 设置一个计数器),/gc 26 个,/memory 14 个,/sched 13 个。值类型只有三种:uint64、float64 和直方图。指标名里带单位(冒号后面),单位变了就是新的键。

读错名字不会崩

实测 · 本机执行输出
  读不存在的指标名:Kind=0(KindBad=0);同一次 Read 里存在的指标 Kind=1 值=0

读一个不存在的指标得到 KindBad,同一次 Read 里其它指标正常。所以升级 Go 版本后某个指标被删了,你的程序不会 panic,但如果不检查 Kind,就会读到 0。

和 MemStats 对得上,除了 goroutine 数

实测 · 本机执行输出
  分配 100 个 1MiB 切片并 GC 一次之后:
    /gc/heap/allocs:bytes 增加 104913992  ←→  MemStats.TotalAlloc 增加 104913992
    /gc/heap/allocs:objects 增加 331  ←→  MemStats.Mallocs 增加 331
    /gc/cycles/total:gc-cycles = 7  ←→  MemStats.NumGC = 7
    /sched/goroutines:goroutines = 66  ←→  runtime.NumGoroutine() = 51
    /memory/classes/total:bytes = 118708504  ←→  MemStats.Sys = 118708504

分配字节数、对象数、GC 次数、Sys 都与 MemStats 完全一致。goroutine 数不一致不是 bug:源码里 NumGoroutine 调的是 gcount(false)(不含系统 goroutine),指标用的是 gcount(true)。这 15 个差值是系统 goroutine,我是从两处源码推出来的,没有逐个列举它们。

读起来便宜

实测 · 本机执行输出
  metrics.Read(1 个指标,复用切片):188 ns/次,0 次分配
  metrics.Read(全部 107 个指标,复用切片):1934 ns/次,0 次分配
  runtime.ReadMemStats:22760 ns/次,0 次分配

复用同一个 []Sample,读 1 个指标 188 ns,读全部 107 个 1934 ns,都没有分配;ReadMemStats 要 22760 ns。需要周期性采样时更适合 metrics。(这里只比较了耗时,没有测它们对其它 goroutine 的影响。)

直方图

实测 · 本机执行输出
  GOMAXPROCS=1,200 个各自计算约 3 毫秒的 goroutine 同时就绪:/sched/latencies:seconds 直方图共 162 个桶、记录 23 次调度等待
    p50:落在桶 [0.0838861 s, 0.100663 s)
    p90:落在桶 [0.134218 s, 0.167772 s)
    p99:落在桶 [0.167772 s, 0.201327 s)
    最大:落在桶 [0.167772 s, 0.201327 s)

调度等待的直方图有 162 个桶,累计型。在只有一个 P、200 个 goroutine 同时就绪的场景下,等待时间从几十毫秒到约 200 毫秒。只记录了 23 次,而不是 200 次;为什么我没有查,可能是采样,没有验证。要用它算百分位,需要自己遍历桶,上面是我的做法。

文档里的约定

实测 · 本机执行输出
  Thus, users of this API are encouraged to sample supported metrics defined by
  the slice returned by All to remain compatible across Go versions. Of course,
  situations arise where reading specific metrics is critical. For these cases,
  ---
  the "kind" for a given metric is guaranteed not to change. If it must change,
  then a new metric will be introduced with a new key and a new "kind."
  
  ---
  In order to improve ease-of-use, this package promises to never produce the
  following classes of floating-point values: NaN, infinity.
  
  ---
  
  /sched/latencies:seconds
  Distribution of the time goroutines have spent in the scheduler
  in a runnable state before actually running. Bucket counts
  ---
  源码对照:
    runtime/debug.go:180: return int(gcount(false))
    runtime/metrics.go:864: a.gTotal = uint64(gcount(true))

指标的 kind 保证不变、浮点值不会出现 NaN 和无穷大;但指标本身可能被弃用和删除,文档建议对 All 返回的全部指标采样以保持兼容。

分步演示

runtime/metrics
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。指标个数与分类来自 metrics.All();对账用同一个进程里连续读 MemStats 与 metrics(分配 100 个 1MiB 切片并 GC);成本用 testing.Benchmark;直方图实验用 GOMAXPROCS=1;读过的文档与源码:go doc runtime/metrics,runtime/debug.go 的 NumGoroutine,runtime/metrics.go 里 gTotal 的计算
    • 没有验证:系统 goroutine 具体是哪些、直方图只记录 23 次的原因、各指标在 Linux 上的取值差异、读指标对应用的干扰、/godebug 计数器的实际用法、长时间采样的开销
    京ICP备2026067502号-1京公网安备11010502063487号