火焰圖(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 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生成火焰圖的一些理解,不對的地方歡迎指證,一些優化細節由於還涉及數據結構的實現原理,說起來可能篇幅比較多,在這裡就不在多贅述,感興趣的朋友可以一起交流學習


推薦閱讀:
相关文章