go pprof火焰图性能优化

火焰图(flame graph)是性能分析的利器,在go1.1之前的版本我们需要借助go-torch生成,在go1.1后go tool pprof集成了此功能,今天就来说说如何使用其进行性能优化

依赖

go version>=1.1

主题

直接撸代码,下面代码可能会有写多余操作,不过此处只是为了简单演示优化过程:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"time"
)

func main() {
	go func() {
		for {
			LocalTz()

			doSomething([]byte(`{"a": 1, "b": 2, "c": 3}`))
		}
	}()

	fmt.Println("start api server...")
	panic(http.ListenAndServe(":8080", nil))
}

func doSomething(s []byte) {
	var m map[string]interface{}
	err := json.Unmarshal(s, &m)
	if err != nil {
		panic(err)
	}

	s1 := make([]string, 0)
	s2 := ""
	for i := 0; i < 100; i++ {
		s1 = append(s1, string(s))
		s2 += string(s)
	}
}

func LocalTz() *time.Location {
	tz, _ := time.LoadLocation("Asia/Shanghai")
	return tz
}

在你启动http server的地方直接加入导入: _ "net/http/pprof"

然后运行程序后,直接访问: http://127.0.01:8080/debug/pprof/,可以看到go运行的信息:

/debug/pprof/

Types of profiles available:
Count	Profile
55	allocs
0	block
0	cmdline
4	goroutine
55	heap
0	mutex
0	profile
10	threadcreate
0	trace
full goroutine stack dump 
Profile Descriptions:

allocs: A sampling of all past memory allocations
block: Stack traces that led to blocking on synchronization primitives
cmdline: The command line invocation of the current program
goroutine: Stack traces of all current goroutines
heap: A sampling of memory allocations of live objects. You can specify the gc GET parameter to run GC before taking the heap sample.
mutex: Stack traces of holders of contended mutexes
profile: CPU profile. You can specify the duration in the seconds GET parameter. After you get the profile file, use the go tool pprof command to investigate the profile.
threadcreate: Stack traces that led to the creation of new OS threads
trace: A trace of execution of the current program. You can specify the duration in the seconds GET parameter. After you get the trace file, use the go tool trace command to investigate the trace.

使用生成火焰图优化

  1. 获取cpuprofile

获取最近10秒程序运行的cpuprofile,-seconds参数不填默认为30。

go tool pprof http://127.0.0.1:8080/debug/pprof/profile -seconds 10

等10s后会生成一个: pprof.samples.cpu.001.pb.gz文件

2. 生成火焰图

go tool pprof -http=:8081 ~/pprof/pprof.samples.cpu.001.pb.gz

其中-http=:8081会启动一个http服务,端口为8081,然后浏览器会弹出此文件的图解:

图1: 火焰图优化

图中,从上往下是方法的调用栈,长度代表cpu时长。

可以看到一个读本地时区的方法: LocalTz(),居然比一系列字符串操作的: doSomething()方法cpu时长多好几倍。

如果一个项目中频繁有时间转换操作,频繁调用LocalTz()方法,这个开销是惊人的。

3. 优化

优化点一LocalTz():

由于每次请求LocalTz()方法得出来的结果肯定是一样的,所以我们可以把LocalTz()方法的结果用一个全局变量存起来,这样就不用每次都去时区文件了

修改后的代码:

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	_ "net/http/pprof"
	"time"
)

var tz *time.Location

func main() {
	go func() {
		for {
			LocalTz()

			doSomething([]byte(`{"a": 1, "b": 2, "c": 3}`))
		}
	}()

	fmt.Println("start api server...")
	panic(http.ListenAndServe(":8080", nil))
}

func doSomething(s []byte) {
	var m map[string]interface{}
	err := json.Unmarshal(s, &m)
	if err != nil {
		panic(err)
	}

	s1 := make([]string, 0)
	s2 := ""
	for i := 0; i < 100; i++ {
		s1 = append(s1, string(s))
		s2 += string(s)
	}
}

func LocalTz() *time.Location {
	if tz == nil {
		tz, _ = time.LoadLocation("Asia/Shanghai")
	}
	return tz
}

优化后的火焰图:

图2: 火焰图优化


优化后在火焰图中只存在doSomething()方法, 说明LocalTz()和doSomething()比起来几乎可以忽略不计。

优化点二字符串拼接:

从图二火焰图可以看出,其实doSomething()方法大部分时间实在做字符串的拼接,所以此处是很有优化空间的。

修改后的doSomething()方法:

func doSomething(s []byte) {
	var m map[string]interface{}
	err := json.Unmarshal(s, &m)
	if err != nil {
		panic(err)
	}

	s1 := make([]string, 0)
	var buff bytes.Buffer
	for i := 0; i < 100; i++ {
		s1 = append(s1, string(s))
		buff.Write(s)
	}
}

我们使用一个bytes.Buffer类型代替原有的字符串拼接,之后要使用只要buff.String()则可,这里就不在列出。当然buffer并不是线程安全的,如果要考虑并发问题则需做另行打算。

优化后的火焰图:

图3: 火焰图优化字符串拼接


我们以json.Unmarshal项做参考,可以看到concatstring项已经被bytes.(*Buffer).Write代替,而且仅仅是json.Unmarshal的1/2左右,而原来的concatstring是json.Unmarshal的3倍左右

优化点三slice初始化容量:

由于s1这个slice初始化容量为0,在append时,会频繁扩容,带来很大的开销,而此处容量其实是已知项。所以我们可以给他一个初始化容量

优化后的代码:

func doSomething(s []byte) {
	var m map[string]interface{}
	err := json.Unmarshal(s, &m)
	if err != nil {
		panic(err)
	}

	s1 := make([]string, 0, 100)
	var buff bytes.Buffer
	for i := 0; i < 100; i++ {
		s1 = append(s1, string(s))
		buff.Write(s)
	}
}

看看效果:

图4: 火焰图优化slice


可以看到runtime.growslice项已经不存在了。

结语

本文只是自己多使用pprof生成火焰图的一些理解,不对的地方欢迎指证,一些优化细节由于还涉及数据结构的实现原理,说起来可能篇幅比较多,在这里就不在多赘述,感兴趣的朋友可以一起交流学习

编辑于 2019-12-19