Go tool: expvar 自定义度量数据,辅助定位性能瓶颈点

介绍 go 内置工具包 expvar 的使用,能够帮我们实现什么功能,以及它跟pprof 的区别

#go #监控 #自定义
在开发过程中,要想对Go应用存在的性能瓶颈进行剖析,首先就要使用pprof 对不同类型的性能数据进行收集和采样。有两种收集和采样数据的方法。

  • 在微观层面,采用通过运行性能基准测试收集和采样数据的方法,这种方法适用于定位函数或方法实现中存在性能瓶颈点的情形;
  • 在宏观层面,采用独立程序收集和采样数据的方法。但通过独立程序进行性能数据采样时,往往很难快速捕捉到真正的瓶颈点,尤其是对于那些内部结构复杂、业务逻辑过多、内部有较多并发的Go程序。我们在对这样的程序进行性能采样时,真正的瓶颈点很可能被其他数据遮盖。
    那么如何能更高效地捕捉到应用的性能瓶颈点呢?
    我们可以通过部署agent或者其他方式,通过查询应用外部特征而获取的探针类(probing)数据(比如查看应用某端口是否有响应并返回正确的数据或状态码),相比于这些消息,我们可能还想知道一些内省的消息,比如更多的有关应用程序状态的上下文信息。这些上下文信息可以是应用对各类资源的占用信息,比如应用运行占用了多少内存空间,也可以是自定义的性能指标信息,比如单位时间处理的外部请求数量、应答延迟、队列积压量等。
    这个时候,我们就需要 go 官方的另外一个包 expvar

Package expvar provides a standardized interface to public variables, such as operation counters in servers. It exposes these variables via HTTP at /debug/vars in JSON format.

我们可以轻松地使用Go标准库提供的expvar包按统一接口、统一数据格式、一致的指标定义方法输出自定义的度量数据。在本篇博客中,我们就一起来看看如何使用expvar输出自定义的性能度量数据。

一个例子

我们首先来看一个例子

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

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

func main() {
http.Handle("/hi", http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
w.Write([]byte("hi\n"))
}))
fmt.Println(http.ListenAndServe("localhost:8080", nil))
}

然后我们访问一下http://localhost:8080/debug/vars
Pasted image 20240314192240
expvar 返回的是标准格式的JSON。
在默认返回的状态数据中包含了两个字段:cmdline和memstats。
这两个输出数据是expvar包在init函数中就已经发布(Publish)了的变量:

1
2
3
4
5
6
src/expvar/expvar.go
func init() {
http.HandleFunc("/debug/vars", expvarHandler)
Publish("cmdline", Func(cmdline))
Publish("memstats", Func(memstats))
}

cmdline 字段的含义是输出数据的应用名,这里因为是通过go run运行的应用,所以cmdline的值是一个临时路径下的应用。
memstats 输出的数据对应的是runtime.Memstats结构体,反映的是应用在运行期间堆内存分配、栈内存分配及GC的状态。runtime.Memstats结构体的字段可能会随着Go版本的演进而发生变化,其字段具体含义可以参考Memstats结构体中的注释。

通过expvar输出自定义度量数据

同样,我们用一个例子说明 如何自定义度量数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main  

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

var customVar = new(expvar.Map).Init()

func init() {
customVar.Set("hi_count", new(expvar.Int))
expvar.Publish("custom", customVar)
}
func main() {
http.Handle("/hi", http.HandlerFunc(func(w http.ResponseWriter,
r *http.Request) {
defer func() {
customVar.Add("hi_count", 1)
}()
w.Write([]byte("hi\n"))
}))
fmt.Println(http.ListenAndServe("localhost:8080", nil))
}

如以上示例所示,定义一个expvar.Map类型变量后,可以向该复合指标变量中添加指标,比如示例中的“hi_count”
然后重新运行例子。先调用两次 localhost:8080/hi

1
2
3
4
5
6
7
8
9
10
➜  test git:(main) ✗ curl localhost:8080/hi
hi
➜ test git:(main) ✗ curl localhost:8080/hi
hi
➜ test git:(main) ✗ curl http://localhost:8080/debug/vars
{
"cmdline": ["/Users/hxzhouh/Library/Caches/JetBrains/GoLand2023.3/tmp/GoLand/___go_build_github_com_hxzhouh_go_example_expvar"],
"custom": {"hi_count": 2},
"memstats":"....."
}

expvar 输出了我们想要的结果。实际上expvar 可以输出任何我们想要的结果,这里就不展开描述。

输出数据的展示

通过/debug/vars服务端点,我们可以得到标准JSON格式的应用内部状态数据,数据采集出来后可根据不同开发者的需求进行转换和展示。JSON格式文本很容易反序列化,开发者可自行解析后使用,比如:编写一个Prometheus exporter,将数据导入Prometheus背后的存储(比如InfluxDB)中,并利用一些基于Web图形化的方式直观展示出来;或者导入Elasticsearch,再通过Kibana或Grafana的页面展示出来。
我们这里展示go开发者Ivan Daniluk 开发的 expvarmon, 首先安装它

1
go get github.com/divan/expvarmon

然后查看数据

1
expvarmon -ports="8080" -vars="custom.hi_count,mem:memstats.Alloc,mem:memstats.Sys,mem:memstats.HeapAlloc,mem:memstats.HeapInuse,duration:memstats.PauseNs,duration:memstats.PauseTotalNs"

结果很geek
Pasted image 20240314195301

expvar包不仅可用于辅助缩小定位性能瓶颈的范围,还可以用来输出度量数据以对应用的运行状态进行监控,这样当程序出现问题时,我们可以快速发现问题并利用输出的度量数据对程序进行诊断并快速定位问题

跟 pprof 的区别

pprof 是 Go 语言另一个内置的性能剖析工具,主要用于分析程序的 CPU 使用情况和内存占用情况。与 expvar 提供实时监控服务内部变量不同,pprof 更多地用于性能分析和优化。简而言之,expvar 能够帮助我们观察程序的”活动”数据,而 pprof 专注于程序的”性能”数据。
expvar 以简单的方式公开应用程序的实时状态数据,如当前活跃连接数或已处理请求的计数器。其设计意图是长期运行以监控应用状态,对于想要快速了解应用健康状况的开发者而言,这是一个非常直接的工具。

总结

expvar 是一个很强大的工具,他可以帮助我们实现应用内情况的监控,并且很方便的跟其他监控平台对接,是每个gopher 都应该掌握的工具。