Golang pprof

1. 簡介

Golang 是一個對性能特別看重的語言,因此語言中自帶了 一些性能分析工具。可以生成相應的Profile(概要文件),譬如CPU Profile、Memory Profile、Block Profile、Mutex Profile、Goroutine Profile等等

CPU Profile:報告程序的 CPU 使用情況,按照一定頻率採集應用程序在 CPU 和寄存器上面的數據

Memory Profile(Heap Profile):報告程序的內存使用情況

Block Profile:報告 goroutines 不在運行狀態的情況,可以用來分析和查找死鎖等性能瓶頸問題

Goroutine Profile:報告 goroutines 的使用情況,有哪些 goroutine,它們的調用關係是怎樣的

2. 使用方法

pprof命令:

usage:

Produce output in the specified format.

   pprof <format> [options] [binary] <source> ...

Omit the format to get an interactive shell whose commands can be used
to generate various views of a profile

   pprof [options] [binary] <source> ...

Omit the format and provide the "-http" flag to get an interactive web
interface at the specified host:port that can be used to navigate through
various views of a profile.

   pprof -http [host]:[port] [options] [binary] <source> ...

Details:
  Output formats (select at most one):
    -callgrind       Outputs a graph in callgrind format
    -comments        Output all profile comments
    -disasm          Output assembly listings annotated with samples
    -dot             Outputs a graph in DOT format
    -eog             Visualize graph through eog
    -evince          Visualize graph through evince
    -gif             Outputs a graph image in GIF format
    -gv              Visualize graph through gv
    -kcachegrind     Visualize report in KCachegrind
    -list            Output annotated source for functions matching regexp
    -pdf             Outputs a graph in PDF format
    -peek            Output callers/callees of functions matching regexp
    -png             Outputs a graph image in PNG format
    -proto           Outputs the profile in compressed protobuf format
    -ps              Outputs a graph in PS format
    -raw             Outputs a text representation of the raw profile
    -svg             Outputs a graph in SVG format
    -tags            Outputs all tags in the profile
    -text            Outputs top entries in text form
    -top             Outputs top entries in text form
    -topproto        Outputs top entries in compressed protobuf format
    -traces          Outputs all profile samples in text form
    -tree            Outputs a text rendering of call graph
    -web             Visualize graph through web browser
    -weblist         Display annotated source in a web browser

  Options:
    -call_tree       Create a context-sensitive call tree
    -compact_labels  Show minimal headers
    -divide_by       Ratio to divide all samples before visualization
    -drop_negative   Ignore negative differences
    -edgefraction    Hide edges below <f>*total
    -focus           Restricts to samples going through a node matching regexp
    -hide            Skips nodes matching regexp
    -ignore          Skips paths going through any nodes matching regexp
    -mean            Average sample value over first value (count)
    -nodecount       Max number of nodes to show
    -nodefraction    Hide nodes below <f>*total
    -normalize       Scales profile based on the base profile.
    -output          Output filename for file-based outputs
    -positive_percentages Ignore negative samples when computing percentages
    -prune_from      Drops any functions below the matched frame.
    -relative_percentages Show percentages relative to focused subgraph
    -sample_index    Sample value to report (0-based index or name)
    -show            Only show nodes matching regexp
    -source_path     Search path for source files
    -tagfocus        Restricts to samples with tags in range or matched by regex
p
    -taghide         Skip tags matching this regexp
    -tagignore       Discard samples with tags in range or matched by regexp
    -tagshow         Only consider tags matching this regexp
    -trim            Honor nodefraction/edgefraction/nodecount defaults
    -unit            Measurement units to display

  Option groups (only set one per group):
    cumulative
      -cum             Sort entries based on cumulative weight
      -flat            Sort entries based on own weight
    granularity
      -addresses       Aggregate at the function level.
      -addressnoinlines Aggregate at the function level, including functions' ad
dresses in the output.
      -files           Aggregate at the file level.
      -functions       Aggregate at the function level.
      -lines           Aggregate at the source code line level.
      -noinlines       Aggregate at the function level.

binary 是應用的二進制文件,用來解析各種符號;source 表示 profile 數據的來源,可以是本地的文件,也可以是 http 地址

在pprof界面,通過 TopN 命令顯示重要信息。

如果我們在top命令後加入標籤–cum,那麼輸出的列表就是以累積取樣計數爲順序的

當開啓CPU Profile時,Go程序每秒鐘進行100次採樣。

The web command writes a graph of the profile data in SVG format and opens it in a web browser. (There is also a gv command that writes PostScript and opens it in Ghostview. For either command, you need graphviz installed.)

通過 Web 命令以SVG格式創建 Profile數據表,並且在web瀏覽器中進行展示。

同時,也可以通過gv命令寫PostScript,並且在Ghostview中打開。不管哪一個命令,都需要安裝graphviz。

在圖中看到耗時的操作時,譬如 function1,那麼可以通過如下命令仔細觀察:

web function1

可以通過如下命令查看源碼

list function1

go tool pprof 最簡單的使用方式爲 go tool pprof [binary] [source],binary 是應用的二進制文件,用來解析各種符號;source 表示 Profile 數據的來源,可以是本地的文件,也可以是 http 地址。

如果程序名爲testDemo,profile文件爲testDemo.prof,那麼通過如下命令就能展示程序的一些運行信息:

go tool pprof testDemo testDemo.prof

那麼問題來了,如何生成Profile文件?

3. Profile文件產生方法

根據程序的性質不同,生成profile文件的方式也不同。

3.1 Web應用

對於Web應用,非常簡單,如果使用了默認的 http.DefaultServeMux(通常是代碼直接使用 http.ListenAndServe(“0.0.0.0:8000”, nil)),在命令源碼文件中導入net/http/pprof包就可以,具體形式如下:

import _ "net/http/pprof"

如果你使用自定義的 Mux,則需要手動註冊一些路由規則:

r.HandleFunc("/debug/pprof/", pprof.Index)
r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
r.HandleFunc("/debug/pprof/profile", pprof.Profile)
r.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
r.HandleFunc("/debug/pprof/trace", pprof.Trace)

3.2 普通應用

方法一

對於普通的應用程序,可以採用和Web應用相同的方法,在main函數中添加如下代碼:

import "net/http"
import "log"
go func() {
	log.Println(http.ListenAndServe("localhost:6060", nil))
}()

或者

package main

import (
    "net/http"
    _ "net/http/pprof"
)

func hiHandler(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("hi"))
}

func main() {
    http.HandleFunc("/", hiHandler)
    http.ListenAndServe(":8080", nil)
}

**方法二 **

主動將各種概要文件寫入文件。

CPU概要文件

對於CPU Profile,可以採用如下方式,在main函數的入口處調用 WriteCPUProfile 函數:

var cpuProfile = flag.String("cpuProfile", "", "write cpu profile to file")
func WriteCPUProfile() {
	if *cpuProfile != "" {
		f, err := os.Create(*cpuProfile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s",
				err)
			return
		}
		if err := pprof.StartCPUProfile(f); err != nil {
			fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err)
			f.Close()
			return
		}
		
		defer pprof.StopCPUProfile() // 把記錄的概要信息寫到已指定的文件
	}
}

或者


func startCPUProfile() {
	if *cpuProfile != "" {
		f, err := os.Create(*cpuProfile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Can not create cpu profile output file: %s",
				err)
			return
		}
		if err := pprof.StartCPUProfile(f); err != nil {
			fmt.Fprintf(os.Stderr, "Can not start cpu profile: %s", err)
			f.Close()
			return
		}
	}
}

停止函數

func stopCPUProfile() {
	if *cpuProfile != "" {
		pprof.StopCPUProfile() // 把記錄的概要信息寫到已指定的文件
	}
}

調用方法

var cpuProfile  = flag.String("cpuprofile", "", "write a cpu profile to the named file during execution")
func main(){
    startCPUProfile()
    defer stopCPUProfile()
}

-cpuprofile 指定CPU概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。

使用方法:

go run testDemo.go -p="runtime" -cpuprofile="./pprof/cpu.pprof"

-p是對命令源碼文件 testDemo.go 有效的。其含義是指定要解析依賴關係的代碼包的導入路徑。(明確)

內存概要文件

對於內存概要函數,分爲start和stop兩個函數:

var memProfile       = flag.String("memprofile", "", "write a memory profile to the named file after execution")
var memProfileRate   = flag.Int("memprofilerate", 0, "if > 0, sets runtime.MemProfileRate")
func startMemProfile() {
	if *memProfile != "" && *memProfileRate > 0 {
		runtime.MemProfileRate = *memProfileRate
	}
}

memProfile是內存概要文件的路徑。memProfileRate的含義是分析器的取樣間隔,單位是字節。如果我們不給runtime.MemProfileRate變量賦值,內存使用情況的取樣操作也會照樣進行。此取樣操作會從用戶程序開始時啓動,且一直持續進行到用戶程序結束。runtime.MemProfileRate變量的默認值是512K字節。只有當我們顯式的將0賦給runtime.MemProfileRate變量之後,纔會取消取樣操作。

在默認情況下,內存使用情況的取樣數據只會被保存在運行時內存中,而保存到文件的操作只能由我們自己來完成。

停止函數:

func stopMemProfile() {
	if *memProfile != "" {
		f, err := os.Create(*memProfile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Can not create mem profile output file: %s", err)
			return
		}
		if err = pprof.WriteHeapProfile(f); err != nil {
			fmt.Fprintf(os.Stderr, "Can not write %s: %s", *memProfile, err)
		}
		f.Close()
	}
}

stopMemProfile函數的功能是停止對內存使用情況的取樣操作。但是,它只做了將取樣數據保存到內存概要文件的操作。在stopMemProfile函數中,我們調用了函數pprof.WriteHeapProfile,並把代表內存概要文件的文件實例作爲了參數。如果pprof.WriteHeapProfile函數沒有返回錯誤,就說明數據已被寫入到了內存概要文件中。

-memprofile 指定內存概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。

-memprofilerate 定義其值爲n。此標記指定每分配n個字節的堆內存時,進行一次取樣操作。

使用方法:

go run testDemo.go -p="runtime" -memprofile="./pprof/mem.pprof" -memprofilerate=10

程序阻塞概要文件

程序阻塞概要文件用於保存用戶程序中的Goroutine阻塞事件的記錄。

var blockProfile = flag.String("blockprofile", "", "write a goroutine blocking profile to the named file after execution")

var blockProfileRate = flag.Int("blockprofilerate", 1, "if > 0, calls runtime.SetBlockProfileRate()")
func startBlockProfile() {
	if *blockProfile != "" && *blockProfileRate > 0 {
		runtime.SetBlockProfileRate(*blockProfileRate)
	}
}

blockProfile的含義爲程序阻塞概要文件的路徑。blockProfileRate的含義是分析器的取樣間隔,單位是次。默認取樣間隔爲1,即默認情況下,每發生一次Goroutine阻塞事件,分析器就會取樣一次。與內存使用情況記錄一樣,運行時系統對Goroutine阻塞事件的取樣操作也會貫穿於用戶程序的整個運行期。但是,如果我們通過runtime.SetBlockProfileRate函數將這個取樣間隔設置爲0或者負數,那麼這個取樣操作就會被取消。

將保存在運行時內存中的Goroutine阻塞事件記錄存放到指定的文件,代碼如下:

func stopBlockProfile() {
	if *blockProfile != "" && *blockProfileRate >= 0 {
		f, err := os.Create(*blockProfile)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Can not create block profile output file: %s", err)
			return
		}
		if err = pprof.Lookup("block").WriteTo(f, 0); err != nil {
			fmt.Fprintf(os.Stderr, "Can not write %s: %s", *blockProfile, err)
		}
		f.Close()
	}
}

在創建程序阻塞概要文件之後,stopBlockProfile函數會先通過函數pprof.Lookup將保存在運行時內存中的內存使用情況記錄取出,並在記錄的實例上調用WriteTo方法將記錄寫入到文件中。

-blockprofile 指定程序阻塞概要文件的保存路徑。該路徑可以是相對路徑也可以是絕對路徑,但其父路徑必須已存在。

-blockprofilerate 定義其值爲n。此標記指定每發生n次Goroutine阻塞事件時,進行一次取樣操作。

使用方法:

go run testDemo.go -p="runtime" -blockprofile="./pprof/block.pprof" -blockprofilerate=10

通過pprof.Lookup函數取出更多種類的取樣記錄,具體如下:

名稱 說明 取樣頻率
goroutine 活躍Goroutine的信息的記錄。 僅在獲取時取樣一次。
threadcreate 系統線程創建情況的記錄。 僅在獲取時取樣一次。
heap 堆內存分配情況的記錄。 默認每分配512K字節時取樣一次。
block Goroutine阻塞事件的記錄。 默認每發生一次阻塞事件時取樣一次

5. 效果展示

5.1 Web應用

爲方便展示效果,先以Web應用展示Pprof的使用,示例程序見Github

查看堆信息:

go tool pprof http://localhost:9090/debug/pprof/heap

在pprof界面中,輸入web命令:

Web_heap

查看阻塞信息:

go tool pprof http://localhost:9090/debug/pprof/block

查看鎖信息(to look at the holders of contended mutexes, after calling runtime.SetMutexProfileFraction in your program)

go tool pprof http://localhost:9090/debug/pprof/mutex

查看60s的CPU信息

go tool pprof http://localhost:9090/debug/pprof/profile?seconds=60

輸入web指令,輸出信息:

web_cpu
在這裏插入圖片描述

採集5s內的程序執行路徑信息:

wget http://localhost:9090/debug/pprof/trace?seconds=5

如果需要查看所有可用的profile,在瀏覽器中打開:

http://localhost:9090/debug/pprof/

結果:
在這裏插入圖片描述

heap信息如下:
在這裏插入圖片描述

5.2 普通應用

測試程序如下見Github,只進行cpu性能分析.

通過 go install havlak1.go生成可執行程序havlak1

首先產生prof文件:

havlak1 -cpuprofile=havlak1.prof

其次,對prof文件進行分析:

go tool pprof havlak1 havlak1.prof

在pprof操作界面:

F:\ProgramTest\Golang\goBin>go tool pprof havlak1.exe havlak1.prof
File: havlak1.exe
Type: cpu
Time: Jul 25, 2019 at 6:34am (CST)
Duration: 20.10s, Total samples = 25.87s (128.68%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)

輸入以下指令查看最佔CPU的10個函數:

top 10

結果如下:

Showing nodes accounting for 19400ms, 74.99% of 25870ms total
Dropped 107 nodes (cum <= 129.35ms)
Showing top 10 nodes out of 55
      flat  flat%   sum%        cum   cum%
    3540ms 13.68% 13.68%     3620ms 13.99%  runtime.mapaccess1_fast64
    3100ms 11.98% 25.67%     6690ms 25.86%  runtime.scanobject
    2420ms  9.35% 35.02%    12460ms 48.16%  main.FindLoops
    2070ms  8.00% 43.02%     3410ms 13.18%  runtime.mallocgc
    2020ms  7.81% 50.83%     2020ms  7.81%  runtime.heapBitsForObject
    1840ms  7.11% 57.94%     1860ms  7.19%  runtime.greyobject
    1410ms  5.45% 63.39%     2800ms 10.82%  runtime.mapassign_fast64ptr
    1330ms  5.14% 68.53%     3530ms 13.65%  main.DFS
     890ms  3.44% 71.98%      890ms  3.44%  runtime.heapBitsSetType
     780ms  3.02% 74.99%      780ms  3.02%  runtime.memclrNoHeapPointers
top 5 -cum

結果如下:

Showing nodes accounting for 2.50s, 9.66% of 25.87s total
Dropped 107 nodes (cum <= 0.13s)
Showing top 5 nodes out of 55
      flat  flat%   sum%        cum   cum%
         0     0%     0%     12.53s 48.43%  main.main
         0     0%     0%     12.53s 48.43%  runtime.main
         0     0%     0%     12.46s 48.16%  main.FindHavlakLoops
     2.42s  9.35%  9.35%     12.46s 48.16%  main.FindLoops
     0.08s  0.31%  9.66%      9.06s 35.02%  runtime.systemstack

每一行表示一個函數的信息。前兩列表示函數在 CPU 上運行的時間以及百分比;第三列是當前所有函數累加使用 CPU 的比例;第四列和第五列代表這個函數以及子函數運行所佔用的時間和比例(也被稱爲累加值 cumulative),應該大於等於前兩列的值;最後一列就是函數的名字。如果應用程序有性能問題,上面這些信息應該能告訴我們時間都花費在哪些函數的執行上了。

pprof 不僅能打印出最耗時的地方(top),還能列出函數代碼以及對應的取樣數據(list)、彙編代碼以及對應的取樣數據(disasm),而且能以各種樣式進行輸出,比如 svg、gv、callgrind、png、gif等等。

其中一個非常便利的是 web 命令,在交互模式下輸入 web,就能自動生成一個 svg 文件,並跳轉到瀏覽器打開,生成了一個函數調用圖(havlak1_web):

web

在這裏插入圖片描述

Web生成的圖包含了更多的信息,而且可視化的圖像能讓我們更清楚地理解整個應用程序的全貌。圖中每個方框對應一個函數,方框越大代表執行的時間越久(包括它調用的子函數執行時間,但並不是正比的關係);方框之間的箭頭代表着調用關係,箭頭上的數字代表被調用函數的執行時間。

web mapaccess1

在這裏插入圖片描述

6. go test

go test 命令有兩個參數和 pprof 相關,它們分別指定生成的 CPU 和 Memory profiling 保存的文件:

-cpuprofile:cpu profiling 數據要保存的文件地址
-memprofile:memory profiling 數據要報文的文件地址

go test -bench=. -cpuprofile=cpu.prof
go test -bench=. -memprofile=mem.prof
1. go tool pprof cpu.prof 
2. go tool pprof -http=:8080 cpu.prof

7. 火焰圖(待續)

8. 參考文獻

Package pprof

package pprof

Profiling Go Programs

Go 大殺器之性能剖析 PProf

go tool pprof

Go的pprof使用

使用 pprof 和火焰圖調試 golang 應用

Profiling and optimizing Go web applications

google/pprof

實戰Go內存泄露

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章