Go语言内置expvar,基于expvar提供的对基础度量的支持能力,我们可以自定义各种度量(metrics)。但是expvar 仅仅是提供了最底层的度量定义支持,对于一些复杂的度量场景,第三方或自实现的 metrics 包必不可少。

go-metrics包是Go领域使用较多的是metrics包,该包是对Java社区依旧十分活跃的Coda Hale’s Metrics library的不完全Go移植(不得不感慨一下:Java的生态还真是强大)。因此该包在概念上与Coda Hale’s Metrics library是基本保持一致的。go-metrics包在文档方面做的还不够,要理解很多概念性的东西,我们还得回到Coda Hale’s Metrics library的项目文档去挖掘。

go-metrics这样的包是纯工具类的包,没有太多“烧脑”的地方,只需要会用即可,这篇文章我们就来简单地看看如何使用go-metrics在Go应用中增加度量。

1. go-metrics的结构

go-metrics在度量指标组织上采用了与Coda Hale’s Metrics library相同的结构,即使用Metrics Registry(Metrics注册表)。Metrics注册表是一个度量指标的集合:

  1. ┌─────────────┐
  2. ┌──────┤ metric1
  3. └─────────────┘
  4. ┌─────────────────┐ ┌─────────────┐
  5. ├───┘
  6. metric2
  7. Registry ├──────────┤
  8. └─────────────┘
  9. ├───────┐
  10. └──────────────┬──┘ ┌─────────────┐
  11. └──┤ metric3
  12. └─────────────┘
  13. ... ...
  14. ┌─────────────┐
  15. └─────────────┤ metricN
  16. └─────────────┘

go-metrics包将Metrics注册表的行为定义为了一个接口类型:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. type Registry interface {
  3. // Call the given function for each registered metric.
  4. Each(func(string, interface{}))
  5. // Get the metric by the given name or nil if none is registered.
  6. Get(string) interface{}
  7. // GetAll metrics in the Registry.
  8. GetAll() map[string]map[string]interface{}
  9. // Gets an existing metric or registers the given one.
  10. // The interface can be the metric to register if not found in registry,
  11. // or a function returning the metric for lazy instantiation.
  12. GetOrRegister(string, interface{}) interface{}
  13. // Register the given metric under the given name.
  14. Register(string, interface{}) error
  15. // Run all registered healthchecks.
  16. RunHealthchecks()
  17. // Unregister the metric with the given name.
  18. Unregister(string)
  19. // Unregister all metrics. (Mostly for testing.)
  20. UnregisterAll()
  21. }

并提供了一个Registry的标准实现类型StandardRegistry:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. type StandardRegistry struct {
  3. metrics map[string]interface{}
  4. mutex sync.RWMutex
  5. }

我们看到StandardRegistry使用map结构来组织metrics。我们可以通过NewRegistry函数创建了一个基于StandardRegistry的Registry实例:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. func NewRegistry() Registry {
  3. return &StandardRegistry{metrics: make(map[string]interface{})}
  4. }

和标准库的flag或log包的设计方式类似,go-metrics包也在包层面上提供了默认的StandardRegistry实例:DefaultRegistry,这样大多数情况直接使用DefaultRegistry实例即可满足你的需求:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. var DefaultRegistry Registry = NewRegistry()

一旦有了默认Registry实例,我们通常使用下面goroutine并发安全的包级函数GetOrRegister来注册或获取某个度量指标:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. func GetOrRegister(name string, i interface{}) interface{} {
  3. return DefaultRegistry.GetOrRegister(name, i)
  4. }

2. go-metrics的度量类型

go-metrics 继承了其前身 Coda Hale’s Metrics library所支持的几种基本的度量类型,它们是Gauges、Counters、Histograms、Meters和Timers。下面我们就针对这几种基本度量类型逐一说明一下其含义和使用方法。

1) Gauge

Gauge是对一个数值的即时测量值,其反映一个值的瞬时快照,比如我们要度量当前队列中待发送消息数量、当前应用程序启动的goroutine数量,都可以用Gauge这种度量类型实现。

下面的例子使用一个Gauge度量类型度量程序当前启动的goroutine数量:

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. "runtime"
  6. "time"
  7. "github.com/rcrowley/go-metrics"
  8. )
  9. func main() {
  10. g := metrics.NewGauge()
  11. metrics.GetOrRegister("goroutines.now", g)
  12. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  13. })
  14. go func() {
  15. t := time.NewTicker(time.Second) //定时器
  16. for {
  17. select {
  18. case <-t.C:
  19. c := runtime.NumGoroutine()
  20. g.Update(int64(c))
  21. fmt.Println("now="+time.Now().String()+",goroutines now =", g.Value())
  22. }
  23. }
  24. }()
  25. http.ListenAndServe(":8080", nil)
  26. }

启动该程序,并用hey工具发起http请求,我们看到如下输出:
hey.zip
image.png

  1. PS C:\Users\Administrator> hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

image.png
image.png
image.png
go-metrics包提供了将 Registry 中的度量指标格式化输出的接口,我们可以使用该接口将指标情况输出出来,而无需自行输出log,比如上面例子可以改造为下面这样:

  1. // gauge1.go
  2. package main
  3. import (
  4. "log"
  5. "net/http"
  6. "runtime"
  7. "time"
  8. "github.com/rcrowley/go-metrics"
  9. )
  10. func main() {
  11. g := metrics.NewGauge()
  12. metrics.GetOrRegister("goroutines.now", g)
  13. go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
  14. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  15. })
  16. go func() {
  17. t := time.NewTicker(time.Second)
  18. for {
  19. select {
  20. case <-t.C:
  21. c := runtime.NumGoroutine()
  22. g.Update(int64(c))
  23. }
  24. }
  25. }()
  26. http.ListenAndServe(":8080", nil)
  27. }

image.png
同样方式运行上面gauge1.log:

  1. PS C:\Users\Administrator> hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

image.png
image.png
go-metrics包的Log函数必须放在一个单独的goroutine中执行,否则它将阻塞调用它的goroutine的继续执行。但Log函数也是goroutine安全的,其每次输出度量值时其实输出的都是Registry中各个度量值的“快照副本”:

  1. // https://github.com/rcrowley/go-metrics/blob/master/registry.go
  2. func (r *StandardRegistry) Each(f func(string, interface{})) {
  3. metrics := r.registered()
  4. for i := range metrics {
  5. kv := &metrics[i]
  6. f(kv.name, kv.value)
  7. }
  8. }
  9. func (r *StandardRegistry) registered() []metricKV {
  10. r.mutex.RLock()
  11. defer r.mutex.RUnlock()
  12. metrics := make([]metricKV, 0, len(r.metrics))
  13. for name, i := range r.metrics {
  14. metrics = append(metrics, metricKV{
  15. name: name,
  16. value: i,
  17. })
  18. }
  19. return metrics
  20. }

对于Gauge这类的季世志度量,就像上面代码那样,我们都是通过Update直接设置其值的。

2) Counter

Counter 顾名思义计数器!和Gauge相比,其提供了指标增减方法Inc和Dec,如下面代码:

  1. // https://github.com/rcrowley/go-metrics/blob/master/counter.go
  2. type Counter interface {
  3. Clear()
  4. Count() int64
  5. Dec(int64)
  6. Inc(int64)
  7. Snapshot() Counter
  8. }

计数是日常使用较多的度量场景,比如一个服务处理的请求次数就十分适合用计数这个度量指标,下面这段代码演示的就是这一场景:

  1. // counter.go
  2. package main
  3. import (
  4. "log"
  5. "net/http"
  6. "time"
  7. "github.com/rcrowley/go-metrics"
  8. )
  9. func main() {
  10. c := metrics.NewCounter()
  11. metrics.GetOrRegister("total.requests", c)
  12. go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
  13. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  14. c.Inc(1)
  15. })
  16. http.ListenAndServe(":8080", nil)
  17. }

在这段代码中,我们每收到一个http request就在其对应的处理函数中利用Counter的Inc方法增加计数,运行上述代码:

  1. PS D:\Projects\Github\NoobWu\go-samples\metrics-demo> go run .\counter.go
  1. PS C:\Users\Administrator> hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

image.png

3) Meter

Meter这个类型用于测量一组事件发生的速度,比如:web服务的平均处理性能(条/秒),除了平均值,go-metrics的Meter默认还提供1分钟、5分钟和15分钟时间段的平均速度,和 top 命令中的 load average 输出的一分钟、五分钟、以及十五分钟的系统平均负载类似。

统计1分钟、5分钟和15分钟时间段的平均速度:

go-metrics使用 - 图9在 UNIX 负载平均值中起着阻尼系数的作用。因此,UNIX 负载平均值相当于一个指数阻尼的移动平均值。更常见的移动平均数(金融分析师经常使用的类型)只是一个简单的算术平均数,超过一些数据点。

下面的表1显示了各自的平滑和阻尼系数,这些系数是基于第一部分中描述的神奇数字。

go-metrics使用 - 图10 a的值由go-metrics使用 - 图11计算,其中R=1、5或15。从表1中我们看到,对数据变化的修正越大(即aR),结果对这些变化的反应越大,因此我们看到输出中的阻尼(1-aR)越小。

这就是为什么 1 分钟的报告比 15 分钟的报告对负荷的变化反应更快。还要注意的是,对于 1 分钟报告来说,UNIX 负载平均值的最大修正值约为 8%,远没有达到 EXCEL 建议的 20% 或 30%。

下面就是一个用Meter来测量web服务处理性能的例子:

  1. // meter.go
  2. package main
  3. import (
  4. "log"
  5. "net/http"
  6. "time"
  7. "github.com/rcrowley/go-metrics"
  8. )
  9. func main() {
  10. m := metrics.NewMeter()
  11. metrics.GetOrRegister("rate.requests", m)
  12. go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
  13. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  14. m.Mark(1)
  15. })
  16. http.ListenAndServe(":8080", nil)
  17. }

我们用hey给该web server“施压”并查看Meter度量指标的输出结果:

  1. PS D:\Projects\Github\NoobWu\go-samples\metrics-demo> go run .\meter.go

image.png
image.png
如果使用Meter度量服务的最佳性能值,那么需要有持续稳定的“施压”,待1、5、15分钟速率稳定后,这时的值才有意义。Meter的最后一项mean rate是平均值,即服务启动后处理请求的总量与程序运行时间的比值。

4) Histogram

Histogram是直方图,与概率统计学上直方图的概念类似,go-metrics中的Histogram也是用来统计一组数据的统计学分布情况的。除了最小值(min)、最大值(max)、平均值(mean)等,它还测量中位数(median)、第75、90、95、98、99和99.9百分位数。
直方图可以用来度量事件发生的数据分布情况,比如:服务器处理请求时长的数据分布情况,下面就是这样一个例子:

  1. // histogram.go
  2. package main
  3. import (
  4. "log"
  5. "math/rand"
  6. "net/http"
  7. "time"
  8. "github.com/rcrowley/go-metrics"
  9. )
  10. func main() {
  11. s := metrics.NewExpDecaySample(1028, 0.015)
  12. h := metrics.NewHistogram(s)
  13. metrics.GetOrRegister("latency.response", h)
  14. go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
  15. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  16. i := rand.Intn(10)
  17. h.Update(int64(time.Microsecond * time.Duration(i)))
  18. })
  19. http.ListenAndServe(":8080", nil)
  20. }

在上面这个例子中,我们使用一个随机值来模拟服务处理http请求的时间。Histogram需要一个采样算法,go-metrics内置了ExpDecaySample采样。运行上述示例,并使用hey模拟客户端请求,我们得到如下输出:

  1. PS D:\Projects\Github\NoobWu\go-samples\metrics-demo> go run .\histogram.go

image.png

  1. PS C:\Users\Administrator> hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

image.png
Histogram 度量输出的值包括min、max、mean(平均数)、median(中位数)、75、95、99、99.9百分位数上的度量结果。

5) Timer

最后我们来介绍Timer这个度量类型。大家千万别被这度量类型的名称所误导,这并不是一个定时器。
Timer是go-metrics定义的一个抽象度量类型,它可以理解为Histogram和Meter的“合体”,即既度量一段代码的执行频率(rate),又给出这段代码执行时间的数据分布。这一点从Timer的实现亦可以看出来:

  1. // https://github.com/rcrowley/go-metrics/blob/master/timer.go
  2. func NewTimer() Timer {
  3. if UseNilMetrics {
  4. return NilTimer{}
  5. }
  6. return &StandardTimer{
  7. histogram: NewHistogram(NewExpDecaySample(1028, 0.015)),
  8. meter: NewMeter(),
  9. }
  10. }

我们看到一个StandardTimer是由histogram和meter组成的。 我们还是以上面的http server服务为例,我们这次用Timer来度量:

  1. // timer.go
  2. package main
  3. import (
  4. "log"
  5. "math/rand"
  6. "net/http"
  7. "time"
  8. "github.com/rcrowley/go-metrics"
  9. )
  10. func main() {
  11. m := metrics.NewTimer()
  12. metrics.GetOrRegister("timer.requests", m)
  13. go metrics.Log(metrics.DefaultRegistry, time.Second, log.Default())
  14. http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
  15. i := rand.Intn(10)
  16. m.Update(time.Microsecond * time.Duration(i))
  17. })
  18. http.ListenAndServe(":8080", nil)
  19. }
  1. PS D:\Projects\Github\NoobWu\go-samples\metrics-demo> go run .\timer.go

image.png
大家可以看到在这里我们同样用随机数模拟请求的处理时间并传给Timer的Update方法。运行这段代码并用hey压测:

  1. PS C:\Users\Administrator> hey -c 5 -n 1000000 -m GET http://127.0.0.1:8080

image.png
image.png
我们看到Timer度量的输出也的确是Histogram和Meter的联合体!

3. 小结

通过go-metrics包,我们可以很方便地为一个Go应用添加度量指标,go-metrics提供的meter、histogram可以覆盖Go应用基本性能指标需求(吞吐性能、延迟数据分布等)。go-metrics还支持各种指标值导出的,只是这里没有提及,大家可以到go-metrics官网了解详情。

本文涉及的源码可以在这里下载https://github.com/bigwhite/experiments/tree/master/go-metrics

原文

作者:Tony Bai 链接:https://tonybai.com/2021/07/06/add-metrics-for-go-application-using-go-metrics/