关于golang:你不知道的-Go-之-pprof

2次阅读

共计 7941 个字符,预计需要花费 20 分钟才能阅读完成。

简介

Go 有十分多好用的工具,pprof 能够用来剖析一个程序的性能。pprof 有以下 4 种类型:

  • CPU profiling(CPU 性能剖析):这是最常应用的一种类型。用于剖析函数或办法的执行耗时;
  • Memory profiling:这种类型也常应用。用于分析程序的内存占用状况;
  • Block profiling:这是 Go 独有的,用于记录 goroutine 在期待共享资源破费的工夫;
  • Mutex profiling:与 Block profiling 相似,然而只记录因为锁竞争导致的期待或提早。

咱们次要介绍前两种类型。Go 中 pprof 相干的性能在包 runtime/pprof 中。

CPU profiling

pprof 应用非常简单。首先调用 pprof.StartCPUProfile() 启用 CPU profiling。它承受一个 io.Writer 类型的参数,pprof会将剖析后果写入这个 io.Writer 中。为了不便预先剖析,咱们写到一个文件中。

在要剖析的代码后调用 pprof.StopCPUProfile()。那么StartCPUProfile()StopCPUProfile()之间的代码执行状况都会被剖析。不便起见能够间接在 StartCPUProfile() 后,用 defer 调用StopCPUProfile(),即剖析这之后的所有代码。

咱们当初实现一个计算斐波那契数列的第 n 数的函数:

func fib(n int) int {
  if n <= 1 {return 1}

  return fib(n-1) + fib(n-2)
}

而后应用 pprof 剖析一下运行状况:

func main() {f, _ := os.OpenFile("cpu.profile", os.O_CREATE|os.O_RDWR, 0644)
  defer f.Close()
  pprof.StartCPUProfile(f)
  defer pprof.StopCPUProfile()

  n := 10
  for i := 1; i <= 5; i++ {fmt.Printf("fib(%d)=%d\n", n, fib(n))
    n += 3 * i
  }
}

执行 go run main.go,会生成一个cpu.profile 文件。这个文件记录了程序的运行状态。应用 go tool pprof 命令剖析这个文件:

下面用 top 命令查看耗时最高的 10 个函数。能够看到 fib 函数耗时最高,累计耗时 390ms,占了总耗时的 90.70%。咱们也能够应用 top5top20别离查看耗时最高的 5 个 和 20 个函数。

当找到耗时较多的函数,咱们还能够应用 list 命令查看该函数是怎么被调用的,各个调用门路上的耗时是怎么的。list命令后跟一个示意办法名的模式:

咱们晓得应用递归求解斐波那契数存在大量反复的计算。上面咱们来优化一下这个函数:

func fib2(n int) int {
  if n <= 1 {return 1}

  f1, f2 := 1, 1
  for i := 2; i <= n; i++ {f1, f2 = f2, f1+f2}

  return f2
}

改用迭代之后耗时如何呢?咱们来测一下。首先执行 go run main.go 生成 cpu.profile 文件,而后应用 go tool pprof 剖析:

这里 top 看到的列表是空的。因为启用 CPU profiling 之后,运行时每隔 10ms 会中断一次,记录每个 goroutine 以后执行的堆栈,以此来剖析耗时。咱们优化之后的代码,在运行时还没来得及中断就执行完了,因而没有信息。

go tool pprof 执行的所有命令能够通过 help 查看:

Memory profiling

内存剖析有所不同,咱们能够在程序运行过程中随时查看堆内存状况。上面咱们编写一个生成随机字符串,和将字符串反复 n 次的函数:

const Letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func generate(n int) string {
  var buf bytes.Buffer
  for i := 0; i < n; i++ {buf.WriteByte(Letters[rand.Intn(len(Letters))])
  }
  return buf.String()}

func repeat(s string, n int) string {
  var result string
  for i := 0; i < n; i++ {result += s}

  return result
}

编写程序,调用下面的函数,记录内存占用状况:

func main() {f, _ := os.OpenFile("mem.profile", os.O_CREATE|os.O_RDWR, 0644)
  defer f.Close()
  for i := 0; i < 100; i++ {repeat(generate(100), 100)
  }

  pprof.Lookup("heap").WriteTo(f, 0)
}

这里在循环完结后,通过 pprof.Lookup("heap") 查看堆内存的占用状况,并将后果写到文件 mem.profile 中。

运行 go run main.go 生成 mem.profile 文件,而后应用 go tool pprof mem.profile 来剖析:

当然也能够应用 list 命令查看,内存在哪一行调配的:

后果在预期之中,因为字符串拼接要会占用不少长期空间。

pkg/profile

runtime/pprof应用起来有些不便,因为要反复编写关上文件,开启剖析,完结剖析的代码。所以呈现了包装了 runtime/pprof 的库:pkg/profilepkg/profile的 GitHub 仓库地址为:https://github.com/pkg/profile。pkg/profile只是对 runtime/pprof 做了一层封装,让它更好用。应用 pkg/profile 能够将代码简化为一行。应用前须要应用 go get github.com/pkg/profile 获取这个库。

defer profile.Start().Stop()

默认启用的是 CPU profiling,数据写入文件 cpu.pprof。应用它来剖析咱们的fib 程序性能:

$ go run main.go 
2021/06/09 21:10:36 profile: cpu profiling enabled, C:\Users\ADMINI~1\AppData\Local\Temp\profile594431395\cpu.pprof
fib(10)=89
fib(13)=377
fib(19)=6765
fib(28)=514229
fib(40)=165580141
2021/06/09 21:10:37 profile: cpu profiling disabled, C:\Users\ADMINI~1\AppData\Local\Temp\profile594431395\cpu.pprof

控制台会输入剖析后果写入的文件门路。

如果要启用 Memory profiling,能够传入函数选项MemProfile

defer profile.Start(profile.MemProfile).Stop()

另外还能够通过函数选项管制内存采样率,默认为 4096。咱们能够改为 1:

defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()

火焰图

通过命令行查看 CPU 或内存状况不够直观。Bredan Gregg 大神创造了火焰图(Flame Graph)能够很直观地看到内存和 CPU 耗费状况。新版本的 go tool pprof 工具曾经集成了火焰图(我应用的是 Go1.16)。想要生成火焰图,必须装置 graphviz。

在 Mac 上:

brew install graphviz

在 Ubuntu 上:

apt install graphviz

在 Windows 上,官网下载页 http://www.graphviz.org/download/ 有可执行安装文件,下载安装即可。留神设置 PATH 门路。

下面程序生成的 cpu.profile 和 mem.profile 咱们能够间接在网页上查看火焰图。执行上面命令:

go tool pprof -http :8080 cpu.profile

默认会关上浏览器窗口,显示上面的页面:

咱们能够在 VIEW 菜单栏中切换显示火焰图:

能够用鼠标在火焰图上悬停、点击,来查看具体的某个调用。

net/http/pprof

如果线上遇到 CPU 或内存占用过高,该怎么办呢?总不能将下面的 Profile 代码编译到生产环境吧,这无疑会极大地影响性能。net/http/pprof提供了一个办法,不应用时不会造成任何影响,遇到问题时能够开启 profiling 帮忙咱们排查问题。咱们只须要应用 import 这个包,而后在一个新的 goroutine 中调用 http.ListenAndServe() 在某个端口启动一个默认的 HTTP 服务器即可:

import (_ "net/http/pprof")

func NewProfileHttpServer(addr string) {go func() {log.Fatalln(http.ListenAndServe(addr, nil))
  }()}

上面咱们编写一个 HTTP 服务器,将后面示例中的求斐波那契数和反复字符串搬到 Web 上。为了让测试后果更显著一点,我把原来执行一次的函数都执行了 1000 次:

func fibHandler(w http.ResponseWriter, r *http.Request) {n, err := strconv.Atoi(r.URL.Path[len("/fib/"):])
  if err != nil {responseError(w, err)
    return
  }

  var result int
  for i := 0; i < 1000; i++ {result = fib(n)
  }
  response(w, result)
}

func repeatHandler(w http.ResponseWriter, r *http.Request) {parts := strings.SplitN(r.URL.Path[len("/repeat/"):], "/", 2)
  if len(parts) != 2 {responseError(w, errors.New("invalid params"))
    return
  }

  s := parts[0]
  n, err := strconv.Atoi(parts[1])
  if err != nil {responseError(w, err)
    return
  }

  var result string
  for i := 0; i < 1000; i++ {result = repeat(s, n)
  }
  response(w, result)
}

创立 HTTP 服务器,注册处理函数:

func main() {mux := http.NewServeMux()
  mux.HandleFunc("/fib/", fibHandler)
  mux.HandleFunc("/repeat/", repeatHandler)

  s := &http.Server{
    Addr:    ":8080",
    Handler: mux,
  }

  NewProfileHttpServer(":9999")

  if err := s.ListenAndServe(); err != nil {log.Fatal(err)
  }
}

咱们另外启动了一个 HTTP 服务器用于解决 pprof 相干申请。

另外为了测试,我编写了一个程序,始终发送 HTTP 申请给这个服务器:

func doHTTPRequest(url string) {resp, err := http.Get(url)
  if err != nil {fmt.Println("error:", err)
    return
  }

  data, _ := ioutil.ReadAll(resp.Body)
  fmt.Println("ret:", len(data))
  resp.Body.Close()}

func main() {
  var wg sync.WaitGroup
  wg.Add(2)
  go func() {defer wg.Done()
    for {doHTTPRequest(fmt.Sprintf("http://localhost:8080/fib/%d", rand.Intn(30)))
      time.Sleep(500 * time.Millisecond)
    }
  }()

  go func() {defer wg.Done()
    for {doHTTPRequest(fmt.Sprintf("http://localhost:8080/repeat/%s/%d", generate(rand.Intn(200)), rand.Intn(200)))
      time.Sleep(500 * time.Millisecond)
    }
  }()
  wg.Wait()}

应用命令 go run main.go 启动服务器。运行下面的程序始终发送申请给服务器。一段时间之后,咱们能够用浏览器关上http://localhost:9999/debug/pprof/

go tool pprof也反对近程获取 profile 文件:

$ go tool pprof -http :8080 localhost:9999/debug/pprof/profile?seconds=120

其中 seconds=120 示意采样 120s,默认为 30s。后果如下:

能够看出这里除了运行时的耗费,次要就是 fibHandlerrepeatHandler这个解决的耗费了。

当然个别线上不可能把这个端口凋谢进去,因为有很大的平安危险。所以,咱们个别在线上机器 profile 生成文件,将文件下载到本地剖析。下面咱们看到 go tool pprof 会生成一个文件保留在本地,例如我的机器上是C:\Users\Administrator\pprof\pprof.samples.cpu.001.pb.gz。把这个文件下载到本地,而后:

$ go tool pprof -http :8888 pprof.samples.cpu.001.pb.gz

net/http/pprof 实现

net/http/pprof的实现也没什么神秘的中央,无非就是在 net/http/pprof 包的 init() 函数中,注册了一些处理函数:

// src/net/http/pprof/pprof.go
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)
}

http.HandleFunc()会将处理函数注册到默认的 ServeMux 中:

// src/net/http/server.go
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {DefaultServeMux.HandleFunc(pattern, handler)
}

这个 DefaultServeMuxnet/http的包级变量,只有一个实例。为了防止门路抵触,通常咱们不倡议在本人编写 HTTP 服务器的时候应用默认的 DefaultServeMux。个别都是先调用http.NewServeMux() 创立一个新的ServeMux,见下面的 HTTP 示例代码。

再来看 net/http/pprof 包注册的处理函数:

// src/net/http/pprof/pprof.go
func Profile(w http.ResponseWriter, r *http.Request) {
  // ...
  if err := pprof.StartCPUProfile(w); err != nil {
    serveError(w, http.StatusInternalServerError,
      fmt.Sprintf("Could not enable CPU profiling: %s", err))
    return
  }
  sleep(r, time.Duration(sec)*time.Second)
  pprof.StopCPUProfile()}

删掉后面无关的代码,这个函数也是调用 runtime/pprofStartCPUProfile(w)办法开始 CPU profiling,而后睡眠一段时间(这个工夫就是采样距离),最初调用 pprof.StopCPUProfile() 进行采纳。StartCPUProfile()办法传入的是 http.ResponseWriter 类型变量,所以采样后果间接写回到 HTTP 的客户端。

内存 profiling 的实现用了一点技巧。首先,咱们在 init() 函数中没有发现解决内存 profiling 的处理函数。实现上,/debug/pprof/heap门路都会走到 Index() 函数中:

// src/net/http/pprof/pprof.go
func Index(w http.ResponseWriter, r *http.Request) {if strings.HasPrefix(r.URL.Path, "/debug/pprof/") {name := strings.TrimPrefix(r.URL.Path, "/debug/pprof/")
    if name != "" {handler(name).ServeHTTP(w, r)
      return
    }
  }
  // ...
}

最终会走到 handler(name).ServeHTTP(w, r)handler 只是基于 string 类型定义的一个新类型,它定义了 ServeHTTP() 办法:

type handler string

func (name handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {p := pprof.Lookup(string(name))
  // ...
  p.WriteTo(w, debug)
}

删掉其余无关的代码,就剩下下面两行。统计数据将会写入http.ResponseWriter

Benchmark

其实在 Benchmark 时也能够生成 cpu.profilemem.profile 这些剖析文件。咱们在第一个示例的目录下新建一个 bench_test.go 文件:

func BenchmarkFib(b *testing.B) {
  for i := 0; i < b.N; i++ {fib(30)
  }
}

而后执行命令go test -bench . -test.cpuprofile cpu.profile

而后就能够剖析这个 cpu.profile 文件了。

总结

本文介绍了 pprof 工具的应用,以及更方便使用的库 pkg/profile,另外介绍如何应用net/http/pprof 给线上程序加个保险,遇到问题随时能够诊断。没有遇到问题不会对性能有任何影响。

参考

  1. pkg/profile GitHub:https://github.com/pkg/profile
  2. 你不晓得的 Go GitHub:https://github.com/darjun/you-dont-know-go

我的博客:https://darjun.github.io

欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~

正文完
 0