公开和发布度量指标

6.7 公开和发布度量指标

为了确定我们当前正在运行的 Go 程序其性能情况如何,我们常常需要借助各种手段去进行收集和查看指标,而这些指标我们常常称其为度量(Metrics)指标,这些指标能够更好的帮助我们确定应用程序中的问题区域和瓶颈在哪里,并且能够借助一些开源组件来绘制监控图表,非常的方便。

度量指标一般可以分为以下几种类别:

  • 与网络带宽使用率有关的指标。
  • 与处理器,内存,磁盘 I/O 和网络 I/O 相关的指标。
  • 与应用各类执行时间有关的指标。
  • 与应用的各类自定义指标有关。

6.7.1 通过 Expvar

6.7.1.1 公开度量指标

在 Go 中我们可以使用标准库 expvar 来公开和发布指标,该标准库会通过 JSON 格式的 HTTP API 公开你的应用自定义指标,以及 Go 的运行时指标。而 expvar 标准库以 HTTP 的方式默认通过 /debug/vars 接口公开这些公共变量,并且注册指定两类变量,如下:

cmdline   os.Args
memstats  runtime.Memstats
  • cmdline:当前 Go 程序运行时的命令行参数。
  • memstats:当前 Go 程序的运行时统计信息(包括内存、垃圾回收等等)

接下来我们编写一个示例来进行实际使用,看看结果如何,代码如下:

import (
	_ "expvar"
	"log"
	"net/http"
)

func main() {
	_ := http.ListenAndServe(":6060", http.DefaultServeMux)
}

在上述代码中,我们初始化了 expvar 标准库,并且运行了一个 HTTP Server,接下来我们通过浏览器访问 http://127.0.0.1:6060/debug/vars 接口,其输出结果如下:

{
	"cmdline": [
		"/private/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/___go_build_main_go"
	],
	"memstats": {
		"Alloc": 186296,
		"TotalAlloc": 186296,
		"Sys": 72827136,
		"Lookups": 0,
		"Mallocs": 729,
		 ...
		]
	}
}

我们可以看到输出了一大长串的东西,这是因为默认情况下该包注册了 os.Args 和 runtime.Memstats 两个指标,分别对应着 cmdline 和 memstats 属性。如果你对 memstats 中展示的的内存、垃圾回收相关字段有兴趣,可以直接查看官方文档 runtime/#MemStats ,其针对每一个字段都有进行详细的介绍,可以很好地帮助你理解。

而这些指标如果单纯通过肉眼去看,都是一大批密密麻麻的数字,辨识度很低。因此在实际操作中,我们常常都会通过监控平台对其进行采集,然后做成趋势图(例如:Grafana),以此来查看其具体情况分析是否存在问题,这样子会更加的直观,辨识度也更高,同时也能基于此来做应用告警。

另外如果你想在应用程序中获取这些数值,可以通过如下方式:

	expvarFunc := expvar.Get("memstats").(expvar.Func)
	memstats := expvarFunc().(runtime.MemStats)
	fmt.Printf("memstats.GCSys: %d", memstats.GCSys)

6.7.1.2 发布度量指标

在实际的应用程序运行过程中,我们常常需要去自定义一些指标,因为标准库所默认公开的指标,不一定是我们所需要的,也不一定足够,并且还有业务上的指标可能也需要记录,那么面向这种使用需求,我们就可以自定义指标来进行收集和发布了,expvar 一共支持如下数据类型,你可以根据实际情况调整:

func NewInt(name string) *Int {}
func NewFloat(name string) *Float {}
func NewMap(name string) *Map {}
func NewString(name string) *String {}

我们编写一个简单的示例进行应用指标的设置,如下:

var (
	appleCounter      *expvar.Int
	GOMAXPROCSMetrics *expvar.Int
)

func init() {
	appleCounter = expvar.NewInt("apple")
	GOMAXPROCSMetrics = expvar.NewInt("GOMAXPROCS")
	GOMAXPROCSMetrics.Set(int64(runtime.NumCPU()))
}

func main() {
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		appleCounter.Add(1)
		_, _ = w.Write([]byte(`Go 语言编程之旅 `))
	})
	_ := http.ListenAndServe(":6060", http.DefaultServeMux)
}

我们在上述代码中,一共自定义了两个整型指标,分别是 apple 和 GOMAXPROCS,输出结果:

{
	"GOMAXPROCS": 4,
	"apple": 0,
	"cmdline": [
		"/private/var/folders/jm/pk20jr_s74x49kqmyt87n2800000gn/T/___go_build_main_go"
	],
	"memstats": {...}
}

GOMAXPROCS 指标我们用于在程序启动时,初始化设置当前应用的 GOMAXPROCS 数量,便于我们针对一些在容器中的奇妙问题进行排查。而名为 apple 的指标采取的是 Add 方法来追加指标计数,因此我们每次访问 http://127.0.0.1:6060/hello,都会使得 apple 指标新增。

6.7.1.3 自定义类型

刚刚我们看到 expvar 只支持 Int、Float、Map、String 四种数据类型,但是我们在实际应用中往往不止这几个,那么我们应该怎么办呢?

这时候我们只需要遵守 expvar.Var 接口的方式来实现,就可以自定指标的数据类型了,expvar.Var 接口原型如下:

type Var interface {
	String() string
}

我们自定义一个 time.Time 的应用类型指标,用于记录应用的启动时间,代码如下:

var upTimeMetrics     *upTimeVar

type upTimeVar struct {
	value time.Time
}

func (v *upTimeVar) Set(date time.Time) {
	v.value = date
}

func (v *upTimeVar) Add(duration time.Duration) {
	v.value = v.value.Add(duration)
}

func (v *upTimeVar) String() string {
	return v.value.Format(time.UnixDate)
}

然后我们调用 expvar.Publish 方法导出并设置该字段,代码如下:

func init() {
	upTimeMetrics = &upTimeVar{value: time.Now().Local()}
	expvar.Publish("uptime", upTimeMetrics)
}

func main() {
	...
	err := http.ListenAndServe(":6060", http.DefaultServeMux)
	if err != nil {
		log.Fatalf("http.ListenAndServe err: %v", err)
	}
}

重新启动后再次访问 http://127.0.0.1:6060/debug/vars 接口,看到输出结果如下:

{
    "uptime": XXX XXX 27 13:30:12 CST 2020
    ...
}

这样子我们就完成了一个 expvar 的指标的数据类型自定义了。

6.7.1.4 自定义路由

在前面的示例中,我们可以看到是通过 import _ "expvar" 来实现 expvar 的默认引入的,而其与 PProf 类似,这样子调用的话会注册进默认的 htpp.DefaultServeMux 中,expvar 源代码如下:

func init() {
	// 实际注册的是 DefaultServeMux.HandleFunc
	http.HandleFunc("/debug/vars", expvarHandler)
	Publish("cmdline", Func(cmdline))
	Publish("memstats", Func(memstats))
}

而在实际应用程序中,我们使用的常常不是默认的 DefaultServeMux,因此我们需要自定义 expvar 的输出方法来进行特殊处理,代码如下:

func Expvar(c *gin.Context) {
	c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
	first := true
	report := func(key string, value interface{}) {
		if !first {
			fmt.Fprintf(c.Writer, ",\n")
		}
		first = false
		if str, ok := value.(string); ok {
			fmt.Fprintf(c.Writer, "%q: %q", key, str)
		} else {
			fmt.Fprintf(c.Writer, "%q: %v", key, value)
		}
	}

	fmt.Fprintf(c.Writer, "{\n")
	expvar.Do(func(kv expvar.KeyValue) {
		report(kv.Key, kv.Value)
	})
	fmt.Fprintf(c.Writer, "\n}\n")
}

然后再将其方法注册到自定义的路由管理器中,就可以实现类似的效果,并且更加的自由,代码如下:

func NewRouter() *gin.Engine {
	...
	r.GET("/debug/vars", api.Expvar)
}

6.7.2 通过 Prometheus

目前业界中还有另外一个最常用的度量指标暴露方式,那就是结合 Prometheus 的技术栈,使用其所提供的 Go SDK 库 client_golang 来进行公开和发布指标,使用方式也非常简单。

6.7.2.1 四大指标类型

但是在正式开始使用之前,我们需要先介绍 Prometheus 的四大指标类型,分别是 Counter、Gauge、Histogram 以及 Summary,其分别代表着不同的指标使用场景。

6.7.2.1.1 Counter

Counter 类型代表一个累积的指标数据,其单调递增,只增不减。在应用场景中,像是请求次数、错误数量等等,就非常适合用 Counter 来做指标类型,另外 Counter 类型,只有在被采集端重新启动时才会归零。

Counter 类型一共包含两个常规方法,如下:

方法名 作用
Inc 将计数器递增 1。
Add(float64) 将给定值添加到计数器,如果设置的值 < 0,则发生错误。

6.7.2.1.2 Gauge

Gauge 类型代表一个可以任意变化的指标数据,其可增可减。在应用场景中,像是 Go 应用程序运行时的 Goroutine 的数量就可以用该类型来表示,因为其是浮动的数值,并非固定的。

Gauge 类型一共包含六个常规方法,如下:

方法名 作用
Set(float64) 将仪表设置为任意值。
Inc() 将仪表增加 1。
Dec() 将仪表减少 1。
Add(float64) 将给定值添加到仪表,该值如果为负数,那么将导致仪表值减少。
Sub(float64) 从仪表中减去给定值,该值如果为负数,那么将导致仪表值增加。
SetToCurrentTime() 将仪表设置为当前 Unix 时间(以秒为单位)。

6.2.2.1.3 Histogram

Histogram 类型将会在一段时间范围内对数据进行采样(通常是请求持续时间或响应大小等等),并将其计入可配置的存储桶(bucket)中,后续可通过指定区间筛选样本,也可以统计样本总数。

简单来讲,也就是在配置 Histogram 类型时,我们会设置分组区间,例如要分析请求的响应时间,我们可以分为 0-100ms,100-500ms,500-1000ms 等等区间段,那么在 metrics 的上报接口中,将会分为多个维度显示统计情况。

Histogram 类型一共包含一个常规方法,如下:

方法名 作用
Observe(float64) 将一个观察值添加到直方图。

6.2.2.1.4 Summary

Summary 类型将会在一段时间范围内对数据进行采样,但是与 Histogram 类型不同的是 Summary 类型将会存储分位数(在客户端进行计算),而不像 Histogram 类型,根据所设置的区间情况统计存储。

Summary 类型在采样计算后,一共提供三种摘要指标,如下:

  • 样本值的分位数分布情况。
  • 所有样本值的大小总和。
  • 样本总数。

Summary 类型一共包含一个常规方法,如下:

方法名 作用
Observe(float64) 将一个观察值添加到摘要。

6.7.2.2 公开度量指标

我们需要安装 github.com/prometheus/client_golang 依赖并进行 promhttp.Handler 的注册 , 如下代码:

func main() {
	http.Handle("/metrics", promhttp.Handler())
	http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
		_, _ = w.Write([]byte(`Go 语言编程之旅 `))
	})
	_ := http.ListenAndServe(":6060", http.DefaultServeMux)
}

启动程序后,我们访问 http://127.0.0.1:6060/metrics 接口,就看到输出结果如下:

# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 8
# HELP go_info Information about the Go environment.
# TYPE go_info gauge
go_info{version="go1.14"} 1
...

在上述输出中,带 # 号的是注释行,第一行注释是用于解释后面的是怎么样的样本数据,如下:

# HELP go_goroutines Number of goroutines that currently exist.

第二行的注解告诉我们该样本数据的度量指标的类型,如下:

# TYPE go_goroutines gauge

因此 go_goroutines 指标字段的代表当前所存在的 goroutine 数量,其指标类型为 Gauge。

另外我们也是可以基于这四大指标类型去自定义属于应用自己的度量指标,但由于这块知识点会与 Prometheus、Grafana 以及 Alertmanager 产生一定的关系,属于另外的技术栈体系,大家如果对可观察性有更深层次的需要,可以查看 Prometheus 的相关文档,进行更深一步的学习。

6.7.2 小结

在本文中我们介绍了常见的两种度量指标的公开和发布方式,其分别是基于 Expvar 和 Prometheus 的模式,两者各有对应的组件支撑,其一般主体流程是应用公布和发布度量指标,再由特定组件进行采集,最终根据所采集的度量指标进行图表绘制和告警等等二次行为。

同时度量指标的合理设置是非常重要的,因为指标范围设置的过大过小都无法较准确的表示出当前应用的整体情况,因此度量指标的具体设置必须根据应用情况进行调整,且每隔一段时间就要进行审视,以此判断其是否仍然合理有效。



本图书由 煎鱼 ©2020 版权所有,所有文章采用知识署名-非商业性使用-禁止演绎 4.0 国际进行许可。