Golang 本地 pprof 实战

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

开篇

之前我们分享了比较详细的 Golang profile 用法,不过只是理念的话大家可能感触不深。今天我们来看看本地测试怎么进行。毕竟,光说不练假把式,千万不要只在发现 OOM 或者 CPU 飙升了再来关注团队半年甚至一年前写的代码。

写性能测试,应该是个 daily routine,不要放到重构的时候再搞,压力会格外大。

pprof 实战

日常开发中我们的 Unit Test 侧重的是逻辑的正确性,如果对两种实现的性能有顾虑,可以补上 benchmark,如果不熟悉的同学可以看下 解析 Golang 测试 - 原生支持(1)

那如果仅仅跑 benchmark 达不到我们的预期,希望更进一步看看 CPU, 内存的占用情况呢?

其实很简单,直接本地跑 pprof,生成一个 pprof 文件,然后你就可以通过浏览器,或者命令行来判断你的代码逻辑执行过程中主要消耗在哪儿了。养成提前关注性能的习惯,比线上出问题了再跑要好的多。

这里我们准备一个仅供 demo 使用的业务逻辑函数:


type someStruct struct {
	id      int64
	address string
	title   string
}

func alloc() {
	var t *someStruct
	for i := 0; i < 10000; i++ {
		t = new(someStruct)
	}
	_ = t
}
复制代码

做的事情很简单,生成一万个 someStruct。

我们可以直接通过 os.Create 创建一个 os.File 的指针,并传入 pprof.StartCPUProfile 函数,执行业务方法,最后 pprof.StopCPUProfile 即可。


package main

import (
	"os"
	"runtime/pprof"
)

func main() {
	f, err := os.Create("alloc.pprof")
	if err != nil {
		panic(err)
	}
	pprof.StartCPUProfile(f)
	for i := 0; i < 5000; i++ {
		alloc()
	}
	pprof.StopCPUProfile()
	f.Close()
}
复制代码

执行过后,在我们本地目录下就会多一个 alloc.pprof 文件,这就是 pprof 写入的。

剩下的流程就熟悉了,有了一个 pprof 文件,下来就是怎么看结果的问题了。回忆一下,pprof 的命令格式是这样的:

扫描二维码关注公众号,回复: 14453597 查看本文章
go tool pprof <format> [options] [binary] <source> ...

复制代码

其中 source 就是 pprof 数据源,在这个 case 里面就是我们的本地文件 alloc.pprof。如果你想在命令行看结果(比如用 top 命令),可以直接运行

go tool pprof alloc.pprof 
复制代码

系统会弹出如下提示:

image.png

那还是我们在 pprof 命令行下经典的 top 和 list 命令:

image.png

image.png

跟平常用法是一样的。

那如果我想在浏览器看火焰图呢?也很简单,这里复习一下,加上你的 http 端口号即可:

go tool pprof -http=":8080" alloc.pprof 
复制代码

运行之后,会弹出如下提示: Serving web UI on http://localhost:8080

然后你的浏览器会自动打开:

http://localhost:8080/ui/ 这个路径下

image.png

在浏览器里切成火焰图即可

http://localhost:8080/ui/flamegraph 路径下

image.png

整体流程非常简单,这里介绍的意义就是希望大家在日常开发中用起来,单测 + benchmark + pprof 这一个组合。提升自身对代码的自信心,也更有性能保障。

基于 Benchmark 生成 profile

上面我们跑 demo 的时候是直接在 main.go 里面来调用 pprof 包的函数,这未免太麻烦。每次都要搞个 main 函数怎么行,哪怕写到 TestXXX 函数里也显得比较奇怪。

其实不用那么麻烦,上面我们只是示例。

go test 命令其实已经集成了对 pprof 的支持。参照 runtime/pprof 官方文档

运行 go test -bench=. 会运行所有的 benchmarks。 而现在我们只需要加上 -cpuprofile 以及 -memprofile 两个参数就可以实现自动生成 pprof 文件了:

go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
复制代码

如果你觉得命名不好,这里也可以将 cpu.prof 以及 mem.prof 替换成你想要的 pprof 名称,类似我们上面的 alloc.pprof。

这两个参数底层的实现也很简单,如果你有计划自己做一个支持 cpu memory profle 的命令行工具,可以参考一下:

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal("could not create CPU profile: ", err)
        }
        defer f.Close() // error handling omitted for example
        if err := pprof.StartCPUProfile(f); err != nil {
            log.Fatal("could not start CPU profile: ", err)
        }
        defer pprof.StopCPUProfile()
    }

    // ... rest of the program ...

    if *memprofile != "" {
        f, err := os.Create(*memprofile)
        if err != nil {
            log.Fatal("could not create memory profile: ", err)
        }
        defer f.Close() // error handling omitted for example
        runtime.GC() // get up-to-date statistics
        if err := pprof.WriteHeapProfile(f); err != nil {
            log.Fatal("could not write memory profile: ", err)
        }
    }
}
复制代码

可以看到,只是对我们上面手动在 main 函数里写逻辑的封装,并添加了 Heap profile,通常情况下,直接用默认的这个 go test 支持即可。

同样的,我们来写代码看一下效果:

  • 业务代码
package demo

type someStruct struct {
	id      int64
	address string
	title   string
}

func alloc() {
	var t *someStruct
	for i := 0; i < 10000; i++ {
		t = new(someStruct)
	}
	_ = t
}
复制代码
  • benchmark
package demo

import "testing"

func BenchmarkAlloc(b *testing.B) {
	for i := 0; i < 10000; i++ {
		alloc()
	}
}

复制代码

执行 go test -cpuprofile alloc_cpu.pprof -memprofile alloc_mem.pprof -bench . 后,我们会看到目录下多了 alloc_cpu.pprof 和 alloc_mem.pprof 两个文件。

下面我们尝试用浏览器看看效果,运行 go tool pprof -http=:8080 alloc_cpu.pprof

image.png

总结

这里我们结合代码来学习了,如果开发者本地开发完,如何通过 pprof 验证自己代码的性能问题。

通常情况下建议直接写 benchmark,然后用 go test 的 -cpuprofile, -memprofile 两个参数来导出 profile 文件,通过浏览器或命令行分析结果就ok。如果你只要测试个小 demo,也可以直接写到 main 函数,copy 我们上面的逻辑即可。

这里还是建议大家,如果对 benchmark,profile 不熟悉的话,可以复习一下我们之前的文章:

感谢阅读,欢迎评论区交流!

猜你喜欢

转载自juejin.im/post/7129344306154274853