likes
comments
collection
share

可观测性-pprof & expvar

作者站长头像
站长
· 阅读数 17

一、功能以及区别

  1. expvar:
    • 主要功能: 用于公开 Go 程序的内部变量,供外部监控使用。
    • 细节: expvar 包允许你注册命名的变量(如计数器、浮点数、字符串等),这些变量会被公开,并通过 HTTP 接口在 /debug/vars 路径上提供 JSON 输出。
    • 用途: 常常用于监控应用的内部状态,例如监控队列的长度、缓存命中率、任务完成数等。
  1. 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页面的分析方式,只需要包含以下两部:

  1. 运行指令,启动一个web服务:go tool pprof -http=:9090 pprof_standalone1_cpu.prof
  2. 登录到相应界面即可将所得数据以不同视图展示出来;

比如,下面是一份火焰图,这份火焰可以透漏这么几个信息,一个是函数栈,一个是函数耗时;明白这两个目的,在按照下面三个方法,即可看懂火焰图;

可观测性-pprof & expvar

  1. Y轴表示调用栈,最下面的函数也就是函数调用栈最上面的函数,是重点关注对象;
  2. X周表示抽样数量,越宽表示执行时间越长,这个函数可能存在性能问题;
  3. 当我们点到某个函数上,会显示该函数详细信息,再次点击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查看相应数据;

可观测性-pprof & expvar

上图中,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对象,这个对象中包含了队列长度等信息,如红色框所示:可观测性-pprof & expvar

当我们需要使用expvar的时候,需要怎么思考呢?以下是我的思路:

  1. 了解系统中,有哪些状态是我们想要的,比如说队列长度等等,然后将这些变量定义在结构体里,如CustomVar
  2. 看看expvar包中四种变量是否符合咱们对每个状态要求,符合就定义几个对应的变量。不符合,需要自己是一个接口,这个有需求去expvar看看;’
  3. Publish自定义属性;

可观测性-pprof & expvar

大概的结构是这样,自定义变量依赖的是全局变量,对于指标的操作也依赖全局变量。

【注】这里定义一个全局的自定义指针,然后所有的操作依赖这个指针,结果没生效。所以如果线上有需求,还是建议按照上述代码来写吧;

总结

本文从采集数据以及分析数据两个角度介绍了pprof以及expvar的使用。

参考

  1. ChatGPT4
  2. 《Go语言精进之路》