可观测性-pprof & expvar
一、功能以及区别
- expvar:
-
- 主要功能: 用于公开 Go 程序的内部变量,供外部监控使用。
- 细节: expvar 包允许你注册命名的变量(如计数器、浮点数、字符串等),这些变量会被公开,并通过 HTTP 接口在 /debug/vars 路径上提供 JSON 输出。
- 用途: 常常用于监控应用的内部状态,例如监控队列的长度、缓存命中率、任务完成数等。
- pprof:
-
- 主要功能: 提供了详细的程序性能分析功能。
- 细节: pprof 包允许你收集程序的 CPU 使用情况、内存分配的情况、以及其他性能数据。数据可以通过 HTTP 接口(例如 /debug/pprof/)收集或直接保存到文件中。
- 用途:
-
-
- CPU 分析: 确定程序中 CPU-intensive 的部分。
- 内存分析: 查看程序的内存分配情况,找出内存泄露或过度分配的地方。
- 阻塞分析: 查看 goroutines 是否由于某些原因(如通道操作)而被阻塞。
- 互斥分析: 分析和定位互斥锁相关的竞态情况。
-
上述是ChatGPT输出的结果。换句话说,
- expvar提供的信息是为了让我们了解系统内部的现状;
- pprof提供的信息是为了让我们分析系统性能的瓶颈;
下面将从如何获取数据以及如何分析数据两个角度来讨论这两工具;
二、pprof的使用
采集数据
func main() {
// 注册路由
http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
fmt.Println(*request)
writer.Write([]byte("hello"))
})
// 服务对象
s := http.Server{
Addr: "localhost:8080",
}
// 优雅退出
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-c
s.Shutdown(context.Background())
}()
log.Println(s.ListenAndServe())
}
在上述代码中,我们引入了包_ "net/http/pprof"
,这个包的init
函数是这样的:
func init() {
http.HandleFunc("/debug/pprof/", Index)
http.HandleFunc("/debug/pprof/cmdline", Cmdline)
http.HandleFunc("/debug/pprof/profile", Profile)
http.HandleFunc("/debug/pprof/symbol", Symbol)
http.HandleFunc("/debug/pprof/trace", Trace)
}
上述代码的意思是,在默认的路由上注册了以上handle
。那么运行上述代码,就可访问http://localhost:8080/debug/pprof/
查看相应数据;
分析数据
这里只展示web页面的分析方式,只需要包含以下两部:
- 运行指令,启动一个web服务:
go tool pprof -http=:9090 pprof_standalone1_cpu.prof
- 登录到相应界面即可将所得数据以不同视图展示出来;
比如,下面是一份火焰图,这份火焰可以透漏这么几个信息,一个是函数栈,一个是函数耗时;明白这两个目的,在按照下面三个方法,即可看懂火焰图;
- Y轴表示调用栈,最下面的函数也就是函数调用栈最上面的函数,是重点关注对象;
- X周表示抽样数量,越宽表示执行时间越长,这个函数可能存在性能问题;
- 当我们点到某个函数上,会显示该函数详细信息,再次点击root就会恢复设置;
三、expvar的使用
采集数据
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"))
}))
fmt.Println(http.ListenAndServe("localhost:8080", nil))
}
expvar的使用方式和pprof类似,在代码中引入了_ "expvar"
,这个包的init
函数是这样的,
func init() {
http.HandleFunc("/debug/vars", expvarHandler)
Publish("cmdline", Func(cmdline))
Publish("memstats", Func(memstats))
}
上述代码的意思是,在默认的路由上注册了以上handle
。那么运行上述代码,就可访问http://localhost:8080/debug/vars
查看相应数据;
上图中,cmdline字段表示输出数据应名,memstats对应runtime.Memstats
结构体,反应运行期间堆、栈内存分配以及GC状态;如果只能展示这两个字段的信息,其实pprof也够用了。它的的另外一个作用是自定义数据类型;看以下例子:
package main
import (
"expvar"
"fmt"
"net/http"
"time"
)
// 定义结构
type CustomVar struct {
SliceLen int64 `json:"sliceLen"`
Field2 float64 `json:"field2"`
}
// 声明expvar变量
var (
sliceLen expvar.Int
field2 expvar.Float
)
func exportStruct() interface{} {
return CustomVar{
SliceLen: sliceLen.Value(),
Field2: field2.Value(),
}
}
// Publish
func init() {
expvar.Publish("customVar", expvar.Func(exportStruct))
}
func main() {
http.Handle("/hi", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hi"))
}))
slice := make([]int, 7, 10)
// 模拟业务逻辑
go func() {
//... ...
for {
sliceLen.Set(int64(len(slice)))
field2.Add(0.001)
time.Sleep(time.Second)
}
}()
fmt.Println(http.ListenAndServe("localhost:8080", nil))
}
上述例子中,定义了一个CustomVar
对象,这个对象中包含了队列长度等信息,如红色框所示:
当我们需要使用expvar
的时候,需要怎么思考呢?以下是我的思路:
- 了解系统中,有哪些状态是我们想要的,比如说队列长度等等,然后将这些变量定义在结构体里,如
CustomVar
- 看看
expvar
包中四种变量是否符合咱们对每个状态要求,符合就定义几个对应的变量。不符合,需要自己是一个接口,这个有需求去expvar看看;’ Publish
自定义属性;
大概的结构是这样,自定义变量依赖的是全局变量,对于指标的操作也依赖全局变量。
【注】这里定义一个全局的自定义指针,然后所有的操作依赖这个指针,结果没生效。所以如果线上有需求,还是建议按照上述代码来写吧;
总结
本文从采集数据以及分析数据两个角度介绍了pprof以及expvar的使用。
参考
- ChatGPT4
- 《Go语言精进之路》
转载自:https://juejin.cn/post/7293788726235873330