Golang 下的 expvar 和 expvarmon 介绍

Go 语言内置 expvar,基于 expvar 提供的对基础度量的支持能力,我们可以自定义各种度量(metrics)。

该包提供了一种标准化接口用于公共变量,例如针对 server 中的操作计数器。

expvarJSON 格式通过 HTTP/debug/vars 来暴露这些变量。

针对这些公共变量的 setmodify 操作具有原子性;

该包除了会添加 HTTP handler 以外,该包还会注册如下变量:

  • cmdline os.Args
  • memstats runtime.Memstats

当我们使用标准库 net/http 包开发 HTTP 服务时,我们可以使用如下方式进行暴露内部指标:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

$ cat expvar.go
package main

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

func main() {
	http.ListenAndServe(":8080", nil)
}

接下来,我们就会发现直接对外暴露了一个 /debug/varsHTTP EndPoint ,直接访问会出现如下信息。

expvar-endpoint

通过 expvar 包,我们可以很清晰知道程序的命令行参数,以及当前的内存状态。

而在 gin 框架下,我们可以通过如下方式快速的导入。

1
2
router := gin.Default()
router.GET("/debug/vars", gin.WrapH(expvar.Handler()))

但是我们可以看到,expvar 默认注册的内存状态信息返回了一个 json,且 HTTP 这种方式是一个静态指标,对于使用者而言,无法直观的查看状态的变化。

这个时候,我们就可以使用 GitHub 上的 expvarmon 包来进行实时监控程序状态了,该工具可以在命令行终端以图形化的方式实时展示特定的指标数据的变化,我们可以执行如下命令安装并实时查看应用指标变化。

1
2
3
4
5
# install
$ go install github.com/divan/expvarmon@latest

# monitor
$  expvarmon -ports="http://localhost:8081/debug/vars"

当正常连接到 expvar 暴露出来的 endpoint 之后,我们就可以看到如下图。

golang-expvarmon

expvarmon 还可以通过不同的参数来支持多种模式:

1
2
3
4
5
./expvarmon -ports="80"
./expvarmon -ports="23000-23010,http://example.com:80-81" -i=1m
./expvarmon -ports="80,remoteapp:80" -vars="mem:memstats.Alloc,duration:Response.Mean,Counter"
./expvarmon -ports="1234-1236" -vars="Goroutines" -self
./expvarmon -ports="https://user:pass@my.remote.app.com:443" -vars="Goroutines" -self

Multiple apps mode

Single app mode

Arbitrary number of services and variables

为程序增加 Prometheus 格式的 metrics

在传统的单体结构中,我们习惯使用各种命令行工具,比如 tophtopsar 等工具来洞察系统内部的基础信息,而在分布式或者云原生环境中,对于众多微服务的指标性监控,基本上都会采用 Prometheus + Grafana 这一套架构中。

因此,当我们的服务正式上线后,SRE 同学一般需要强制要求业务团队提供基础的 metrics 指标才能允许上线,否则当灾难来临时,整个业务团队和 SRE 团队将会变的更佳被动,从而可能错失重要客户和大单。

所以在运维领域也会经常听到 “无监控,不运维!”

接下来,我们来看看如何为程序提供 prometheus 格式的 metrics,来暴露业务服务的整体情况。

Prometheus 提供了 promtheus-go 库来帮助我们暴露程序的内部指标。

对于 gin 框架的使用者而言,我们只需要增加如下代码,即可在程序中暴露对应的 metrics 指标。

1
2
3
4
5
6
7
8
9
$ cat main.go
....
....
import "github.com/prometheus/client_golang/prometheus/promhttp"

router := gin.New()
router.GET("/metrics", gin.WrapH(promhttp.Handler()))
....
....

当程序正常运行后,我们可以看到程序新增了 /metrics 的 endpoint。

Gin-Metrics

此时,当我们访问 http://host:port/metrics 时,将会看到如下内容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ curl localhost:9091/metrics
# HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 9.7131e-05
go_gc_duration_seconds{quantile="0.25"} 0.000113391
go_gc_duration_seconds{quantile="0.5"} 0.000190793
go_gc_duration_seconds{quantile="0.75"} 0.000437726
go_gc_duration_seconds{quantile="1"} 0.000745738
go_gc_duration_seconds_sum 0.001584779
go_gc_duration_seconds_count 5

....
....

可以看到,我们只增加了几行代码,已经有一些指标数据以 Prometheus 格式输出,这是因为 Go 客户端库默认在我们暴露的全局默认指标注册表中注册了一些关于 promhttp 处理器和运行时间相关的默认指标,根据不同指标名称的前缀可以看出:

  • go_前缀:是关于 Go 运行时相关的指标,比如垃圾回收时间、goroutine 数量等,这些都是 Go 客户端库特有的,其他语言的客户端库可能会暴露各自语言的其他运行时指标。
  • promhttp_前缀:来自 promhttp 工具包的相关指标,用于跟踪对指标请求的处理。

那么需要如何定义自定义指标呢,首先我们需要知道,在 Prometheus 的世界里,指标数据的类型以及结构:

  • Gauges: 表示指标值是可以上升或下降的,在 prometheus 的 sdk 中 暴露了 Set ()、Inc ()、Dec ()、Add () 和 Sub () 这些函数来更改指标值
  • Counters: 代表一种样本数据单调递增的指标,即只增不减,除非监控系统发生了重置。所以该对象下面只有 Inc () 和 Add () 两个函数,而要实际计算趋势的时候,我们一般会使用的 rate () 函数会自动处理
  • Histograms: Prometheus 中的直方图是累积值,即每个区间的数值总计,每一个后续的 bucket 都包含前一个 bucket 的观察计数,所有 bucket 的下限都从 0 开始的。直方图会自动对数值的分布进行分类和计数,所以它只有一个 Observe () 方法,每当你在代码中处理要跟踪的数据时,就会调用这个方法
  • Summaries: 与 Histogram 类似类型,用于表示一段时间内的数据采样结果(通常是请求持续时间或响应大小等),但它直接存储了分位数(通过客户端计算,然后展示出来),而不是通过区间计算
  • Labels: 我们都知道,在prometheus 中,每一个指标项对应到不同的 label 上会形成唯一的一个metrics 来描述具体的指标,比如 /api/user 的请求,使用了label 之后即可能存在 http_request_user_counter{method="GET"}http_request_user_counter{method="POST"} 两个指标

备注: 当然 label 的设计也需要合理,不然当业务比较复杂时,label 会使得整个 指标呈指数级别增长。

这里,我们以 Guages 类型指标来作为示例 【其他类型指标都是类似的方式】。

对于 Guages 指标而言,我们仅需要在代码中额外增加如下代码,便可以在给 metrics 中增加一个自定义的 temperature_celsius 指标。

1
2
3
4
5
6
7
8
9
// 创建一个没有任何 label 标签的 guage 指标
  temp := prometheus.NewGauge(prometheus.GaugeOpts{
    Name: "temperature_celsius",
    Help: "The current temperature in degrees Celsius.",
  })
// 在默认的注册表中注册该指标
  prometheus.MustRegister(temp)
// 设置 gauge 的值为 88
  temp.Set(88)

当程序成功运行时,我们可以看到

1
2
3
4
5
6
$ curl http://localhost:8081/metrics
.....
.....
# HELP temperature_celsius The current temperature in degrees Celsius.
# TYPE temperature_celsius gauge
temperature_celsius 88

需要注意的是,注册时有两个函数:

  • prometheus.MustRegister 函数来将指标注册到全局默认注册中
  • prometheus.NewRegistry 函数来创建和使用自己的非全局的注册

创建一个非全局的注册表,并明确地将其传递给程序中需要注册指标的地方,这也一种更加推荐的做法。

而如果需要使用 带有 label 的指标,我们只需要将初始化类型的方法由 prometheus.NewType 更改为 prometheus.NewTypeVec

比如上面我们 guage 类型的带 label 的指标,将更改为如下方式:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
	temp := prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "temperature_celsius",
			Help: "The current temperature in degrees Celsius.",
		},
		[]string{"cpuType"},
	)
	prometheus.MustRegister(temp)
	temp.With(prometheus.Labels{"cpuType": "AMD"}).Set(88)
	temp.WithLabelValues("Intel").Set(66)

当我们再次访问 metrics 的时候,就会出现如下指标:

1
2
3
4
5
6
7
$ curl http://localhost:8081/metrics
.....
.....
# HELP temperature_celsius The current temperature in degrees Celsius.
# TYPE temperature_celsius gauge
temperature_celsius{cpuType="AMD"} 88
temperature_celsius{cpuType="Intel"} 66

而对于其他类型指标的添加,也和 Guage 类似,有需要的小伙伴可以去 prometheus-go-doc 中查看相关文档。

也希望不论是作为业务开发者或者 SRE 相关从业者,在我们的业务准备上线前,都可以确保提供了一些强指标来暴露服务的整体状态,一起为业务的稳定性、可靠性保驾护航。

附录

Golang 开发者学习路线。