runtime.ReadMemStats 能读的东西有限,而且读一次要 2 万多纳秒。runtime/metrics 是更稳定的替代:每个指标一个字符串键,可以只读你要的。这篇列出全部指标,和 MemStats 对账,量读取成本,并读了一个调度等待的直方图。
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 累计=false107 个指标里 /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。
分配 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 返回的全部指标采样以保持兼容。