Go 程序崩了?煎魚(yú)教你用 PProf 工具來(lái)救火!
本文轉(zhuǎn)載自微信公眾號(hào)「腦子進(jìn)煎魚(yú)了」,作者陳煎魚(yú)。轉(zhuǎn)載本文請(qǐng)聯(lián)系腦子進(jìn)煎魚(yú)了公眾號(hào)。
前言
應(yīng)用程序在運(yùn)行時(shí),總是會(huì)出現(xiàn)一些你所意料不到的問(wèn)題,像是跑著跑著突然報(bào)警,監(jiān)控提示你進(jìn)程 CPU 使用率過(guò)高、內(nèi)存占用不斷增大(疑似泄露)、臨時(shí)內(nèi)存大量申請(qǐng)后長(zhǎng)時(shí)間內(nèi)不滑,又或是 Goroutine 泄露、出現(xiàn) Goroutine 數(shù)量暴漲,并且持續(xù)保持,甚至是莫名其妙在某次迭代發(fā)布后的數(shù)小時(shí)內(nèi)出現(xiàn)了應(yīng)用程序無(wú)法提供服務(wù)的問(wèn)題...
這發(fā)生起來(lái)的話,是多么的讓人感到擔(dān)憂,那么除了在我們平時(shí)要做好各類防護(hù)以外,在問(wèn)題正在發(fā)生時(shí),我們又有什么辦法排查呢,因此在這個(gè)章節(jié),我們將介紹排查辦法之一,也就是 Go 語(yǔ)言的性能剖析大殺器 PProf 工具鏈,它是 Go 語(yǔ)言中必知必會(huì)的技能點(diǎn)。
PProf 是什么
在 Go 語(yǔ)言中,PProf 是用于可視化和分析性能分析數(shù)據(jù)的工具,PProf 以 profile.proto 讀取分析樣本的集合,并生成報(bào)告以可視化并幫助分析數(shù)據(jù)(支持文本和圖形報(bào)告)。
而剛剛提到的 profile.proto 是一個(gè) Protobuf v3 的描述文件,它描述了一組 callstack 和 symbolization 信息, 作用是統(tǒng)計(jì)分析的一組采樣的調(diào)用棧,是很常見(jiàn)的 stacktrace 配置文件格式。
有哪幾種采樣方式
- runtime/pprof:采集程序(非 Server)的指定區(qū)塊的運(yùn)行數(shù)據(jù)進(jìn)行分析。
- net/http/pprof:基于HTTP Server運(yùn)行,并且可以采集運(yùn)行時(shí)數(shù)據(jù)進(jìn)行分析。
- go test:通過(guò)運(yùn)行測(cè)試用例,并指定所需標(biāo)識(shí)來(lái)進(jìn)行采集。
支持什么使用模式
- Report generation:報(bào)告生成。
- Interactive terminal use:交互式終端使用。
- Web interface:Web 界面。
可以做什么
- CPU Profiling:CPU 分析,按照一定的頻率采集所監(jiān)聽(tīng)的應(yīng)用程序 CPU(含寄存器)的使用情況,可確定應(yīng)用程序在主動(dòng)消耗 CPU 周期時(shí)花費(fèi)時(shí)間的位置。
- Memory Profiling:內(nèi)存分析,在應(yīng)用程序進(jìn)行堆分配時(shí)記錄堆棧跟蹤,用于監(jiān)視當(dāng)前和歷史內(nèi)存使用情況,以及檢查內(nèi)存泄漏。
- Block Profiling:阻塞分析,記錄Goroutine阻塞等待同步(包括定時(shí)器通道)的位置,默認(rèn)不開(kāi)啟,需要調(diào)用runtime.SetBlockProfileRate進(jìn)行設(shè)置。
- Mutex Profiling:互斥鎖分析,報(bào)告互斥鎖的競(jìng)爭(zhēng)情況,默認(rèn)不開(kāi)啟,需要調(diào)用runtime.SetMutexProfileFraction進(jìn)行設(shè)置。
- Goroutine Profiling:Goroutine 分析,可以對(duì)當(dāng)前應(yīng)用程序正在運(yùn)行的 Goroutine 進(jìn)行堆棧跟蹤和分析。
其中像是 Goroutine Profiling 這項(xiàng)功能會(huì)在實(shí)際排查中會(huì)經(jīng)常用到。
因?yàn)楹芏鄦?wèn)題出現(xiàn)時(shí)的表象就是 Goroutine 暴增,而這時(shí)候我們要做的事情之一就是查看應(yīng)用程序中的 Goroutine 正在做什么事情,因?yàn)槭裁醋枞?,然后再進(jìn)行下一步。
介紹和使用
一個(gè)簡(jiǎn)單的例子
我們新建一個(gè) main.go 文件,用于后續(xù)的應(yīng)用程序分析和示例展示,寫(xiě)入如下代碼:
- var datas []string
- func main() {
- go func() {
- for {
- log.Printf("len: %d", Add("go-programming-tour-book"))
- time.Sleep(time.Millisecond * 10)
- }
- }()
- _ = http.ListenAndServe("0.0.0.0:6060", nil)
- }
- func Add(str string) int {
- data := []byte(str)
- datas = append(datas, string(data))
- return len(datas)
- }
接下來(lái)最重要的一步,就是在 import 中添加 _ "net/http/pprof" 的引用,如下:
- import (
- _ "net/http/pprof"
- ...
- )
接下來(lái)我們運(yùn)行這個(gè)程序,訪問(wèn) http://127.0.0.1:6060/debug/pprof/ 地址,檢查是否正常響應(yīng)。
通過(guò)瀏覽器訪問(wèn)
第一種方式,我們可以直接通過(guò)瀏覽器,進(jìn)行查看,那么在第一步我們可以先查看總覽頁(yè)面,也就是訪問(wèn) http://127.0.0.1:6060/debug/pprof/,如下:
- /debug/pprof/
- Types of profiles available:
- Count Profile
- 3 allocs
- 0 block
- 0 cmdline
- 8 goroutine
- 3 heap
- 0 mutex
- 0 profile
- 11 threadcreate
- 0 trace
- full goroutine stack dump
- allocs:查看過(guò)去所有內(nèi)存分配的樣本,訪問(wèn)路徑為$HOST/debug/pprof/allocs。
- block:查看導(dǎo)致阻塞同步的堆棧跟蹤,訪問(wèn)路徑為$HOST/debug/pprof/block。
- cmdline:當(dāng)前程序的命令行的完整調(diào)用路徑。
- goroutine:查看當(dāng)前所有運(yùn)行的 goroutines 堆棧跟蹤,訪問(wèn)路徑為$HOST/debug/pprof/goroutine。
- heap:查看活動(dòng)對(duì)象的內(nèi)存分配情況, 訪問(wèn)路徑為$HOST/debug/pprof/heap。
- mutex:查看導(dǎo)致互斥鎖的競(jìng)爭(zhēng)持有者的堆棧跟蹤,訪問(wèn)路徑為$HOST/debug/pprof/mutex。
- profile:默認(rèn)進(jìn)行 30s 的 CPU Profiling,得到一個(gè)分析用的 profile 文件,訪問(wèn)路徑為$HOST/debug/pprof/profile。
- threadcreate:查看創(chuàng)建新OS線程的堆棧跟蹤,訪問(wèn)路徑為$HOST/debug/pprof/threadcreate。
如果你在對(duì)應(yīng)的訪問(wèn)路徑上新增 ?debug=1 的話,就可以直接在瀏覽器訪問(wèn),如下:
debug 模式
若不新增 debug 參數(shù),那么將會(huì)直接下載對(duì)應(yīng)的profile文件。
再展開(kāi)來(lái)講,在部署環(huán)境中,我們?yōu)榱司W(wǎng)絡(luò)安全,通常不會(huì)直接對(duì)外網(wǎng)暴露 pprof 的相關(guān)端口,因此會(huì)通過(guò) curl、wget 等方式進(jìn)行 profile 文件的間接拉取。
另外還有一點(diǎn)需要注意,debug 的訪問(wèn)方式是具有時(shí)效性的,在實(shí)際場(chǎng)景中,我們常常需要及時(shí)將當(dāng)前狀態(tài)下的 profile 文件給存儲(chǔ)下來(lái),便于二次分析。
通過(guò)交互式終端使用
第二種方式,我們可以直接通過(guò)命令行,來(lái)完成對(duì)正在運(yùn)行的應(yīng)用程序 pprof 的抓取和分析。
CPU Profiling
- $ go tool pprof http://localhost:6060/debug/pprof/profile\?seconds\=60
- Fetching profile over HTTP from http://localhost:6060/debug/pprof/profile?seconds=60
- Saved profile in /Users/eddycjy/pprof/pprof.samples.cpu.002.pb.gz
- Type: cpu
- Duration: 1mins, Total samples = 37.25s (61.97%)
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof)
執(zhí)行該命令后,需等待 60 秒(可調(diào)整 seconds 的值),pprof 會(huì)進(jìn)行 CPU Profiling,結(jié)束后將默認(rèn)進(jìn)入 pprof 的命令行交互式模式,可以對(duì)分析的結(jié)果進(jìn)行查看或?qū)С?。另外如果你所啟?dòng)的 HTTP Server 是 TLS 的方式,那么在調(diào)用 go tool pprof 時(shí),需要將調(diào)用路徑改為:go tool pprof https+insecure://localhost:6060/debug/pprof/profile\?seconds\=60。
接下來(lái)我們輸入查詢命令 top10 ,以此查看對(duì)應(yīng)資源開(kāi)銷(例如:CPU 就是執(zhí)行耗時(shí)/開(kāi)銷、Memory 就是內(nèi)存占用大小)排名前十的函數(shù),如下:
- (pprof) top10
- Showing nodes accounting for 36.23s, 97.26% of 37.25s total
- Dropped 80 nodes (cum <= 0.19s)
- Showing top 10 nodes out of 34
- flat flat% sum% cum cum% Name
- 32.63s 87.60% 87.60% 32.70s 87.79% syscall.syscall
- 0.87s 2.34% 89.93% 0.88s 2.36% runtime.stringtoslicebyte
- 0.69s 1.85% 91.79% 0.69s 1.85% runtime.memmove
- 0.52s 1.40% 93.18% 0.52s 1.40% runtime.nanotime
- ...
- (pprof)
- flat:函數(shù)自身的運(yùn)行耗時(shí)。
- flat%:函數(shù)自身在 CPU 運(yùn)行耗時(shí)總比例。
- sum%:函數(shù)自身累積使用 CPU 總比例。
- cum:函數(shù)自身及其調(diào)用函數(shù)的運(yùn)行總耗時(shí)。
- cum%:函數(shù)自身及其調(diào)用函數(shù)的運(yùn)行耗時(shí)總比例。
- Name:函數(shù)名。
在大多數(shù)的情況下,我們可以通過(guò)這五列得出一個(gè)應(yīng)用程序的運(yùn)行情況,知道當(dāng)前是什么函數(shù),正在做什么事情,占用了多少資源,誰(shuí)又是占用資源的大頭,以此來(lái)得到一個(gè)初步的分析方向。
另外在交互命令行中,pprof 還支持了大量的其它命令,具體可執(zhí)行 pprof help 查看幫助說(shuō)明。
Heap Profiling
- $ go tool pprof http://localhost:6060/debug/pprof/heap
- Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap
- Saved profile in /Users/eddycjy/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.011.pb.gz
- Type: inuse_space
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof)
執(zhí)行該命令后,能夠很快的拉取到其結(jié)果,因?yàn)樗恍枰?CPU Profiling 做采樣等待,這里需要注意的一點(diǎn)是 Type 這一個(gè)選項(xiàng),你可以看到它默認(rèn)顯示的是 inuse_space,實(shí)際上可以針對(duì)多種內(nèi)存概況進(jìn)行分析,常用的類別如下:
- inuse_space:分析應(yīng)用程序的常駐內(nèi)存占用情況。
- $ go tool pprof -inuse_space http://localhost:6060/debug/pprof/heap
- (pprof) top
- Showing nodes accounting for 4.01GB, 100% of 4.01GB total
- flat flat% sum% cum cum%
- 4.01GB 100% 100% 4.01GB 100% main.Add
- 0 0% 100% 4.01GB 100% main.main.func1
- alloc_objects:分析應(yīng)用程序的內(nèi)存臨時(shí)分配情況。
- $ go tool pprof -alloc_objects http://localhost:6060/debug/pprof/heap
- (pprof) top
- Showing nodes accounting for 215552550, 100% of 215560746 total
- Dropped 14 nodes (cum <= 1077803)
- flat flat% sum% cum cum%
- 86510197 40.13% 40.13% 86510197 40.13% main.Add
- 85984544 39.89% 80.02% 85984544 39.89% fmt.Sprintf
- 43057809 19.97% 100% 215552550 100% main.main.func1
- 0 0% 100% 85984544 39.89% log.Printf
另外還有 inuse_objects 和 alloc_space 類別,分別對(duì)應(yīng)查看每個(gè)函數(shù)所分別的對(duì)象數(shù)量和查看分配的內(nèi)存空間大小,具體可根據(jù)情況選用。
Goroutine Profiling
- $ go tool pprof http://localhost:6060/debug/pprof/goroutine
- Fetching profile over HTTP from http://localhost:6060/debug/pprof/goroutine
- Saved profile in /Users/eddycjy/pprof/pprof.goroutine.003.pb.gz
- Type: goroutine
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof)
在查看 goroutine 時(shí),我們可以使用traces命令,這個(gè)命令會(huì)打印出對(duì)應(yīng)的所有調(diào)用棧,以及指標(biāo)信息,可以讓我們很便捷的查看到整個(gè)調(diào)用鏈路有什么,分別在哪里使用了多少個(gè) goroutine,并且能夠通過(guò)分析查看到誰(shuí)才是真正的調(diào)用方,輸出結(jié)果如下:
- (pprof) traces
- Type: goroutine
- -----------+-------------------------------------------------------
- 2 runtime.gopark
- runtime.netpollblock
- internal/poll.runtime_pollWait
- ...
- -----------+-------------------------------------------------------
- 1 runtime.gopark
- runtime.netpollblock
- ...
- net/http.ListenAndServe
- main.main
- runtime.main
在調(diào)用棧上來(lái)講,其展示順序是自下而上的,也就是 runtime.main 方法調(diào)用了 main.main 方法,main.main 方法又調(diào)用了 net/http.ListenAndServe 方法,這里對(duì)應(yīng)的也就是我們所使用的示例代碼了,排查起來(lái)會(huì)非常方便。
每個(gè)調(diào)用堆棧信息用 ----------- 分割,函數(shù)方法前的就是指標(biāo)數(shù)據(jù),像 Goroutine Profiling 展示是就是該方法占用的 goroutine 的數(shù)量。而 Heap Profiling 展示的就是占用的內(nèi)存大小,如下:
- $ go tool pprof http://localhost:6060/debug/pprof/heap
- ...
- Type: inuse_space
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof) traces
- Type: inuse_space
- -----------+-------------------------------------------------------
- bytes: 13.55MB
- 13.55MB main.Add
- main.main.func1
- -----------+-------------------------------------------------------
實(shí)際上 pprof 中的所有功能都會(huì)根據(jù)不同的 Profile 類型展示不同的對(duì)應(yīng)結(jié)果。
Mutex Profiling
怎么樣的情況下會(huì)造成阻塞呢,一般有如下方式:調(diào)用 chan(通道)、調(diào)用 sync.Mutex (同步鎖)、調(diào)用 time.Sleep() 等等。那么為了驗(yàn)證互斥鎖的競(jìng)爭(zhēng)持有者的堆棧跟蹤,我們可以根據(jù)以上的 sync.Mutex 方式,來(lái)調(diào)整先前的示例代碼,代碼如下:
- func init() {
- runtime.SetMutexProfileFraction(1)
- }
- func main() {
- var m sync.Mutex
- var datas = make(map[int]struct{})
- for i := 0; i < 999; i++ {
- go func(i int) {
- m.Lock()
- defer m.Unlock()
- datas[i] = struct{}{}
- }(i)
- }
- _ = http.ListenAndServe(":6061", nil)
- }
需要特別注意的是 runtime.SetMutexProfileFraction 語(yǔ)句,如果未來(lái)希望進(jìn)行互斥鎖的采集,那么需要通過(guò)調(diào)用該方法來(lái)設(shè)置采集頻率,若不設(shè)置或沒(méi)有設(shè)置大于 0 的數(shù)值,默認(rèn)是不進(jìn)行采集的。
接下來(lái)我們進(jìn)行調(diào)用 go tool pprof 進(jìn)行分析,如下:
- $ go tool pprof http://localhost:6061/debug/pprof/mutex
- Fetching profile over HTTP from http://localhost:6061/debug/pprof/mutex
- Saved profile in /Users/eddycjy/pprof/pprof.contentions.delay.010.pb.gz
- Type: delay
- Entering interactive mode (type "help" for commands, "o" for options)
我們查看調(diào)用 top 命令,查看互斥量的排名:
- (pprof) top
- Showing nodes accounting for 653.79us, 100% of 653.79us total
- flat flat% sum% cum cum%
- 653.79us 100% 100% 653.79us 100% sync.(*Mutex).Unlock
- 0 0% 100% 653.79us 100% main.main.func1
接下來(lái)我們可以調(diào)用 list 命令,看到指定函數(shù)的代碼情況(包含特定的指標(biāo)信息,例如:耗時(shí)),若函數(shù)名不明確,默認(rèn)會(huì)對(duì)函數(shù)名進(jìn)行模糊匹配,如下:
- (pprof) list main
- Total: 653.79us
- ROUTINE ======================== main.main.func1 in /eddycjy/main.go
- 0 653.79us (flat, cum) 100% of Total
- . . 40: go func(i int) {
- . . 41: m.Lock()
- . . 42: defer m.Unlock()
- . . 43:
- . . 44: datas[i] = struct{}{}
- . 653.79us 45: }(i)
- . . 46: }
- . . 47:
- . . 48: err := http.ListenAndServe(":6061", nil)
- . . 49: if err != nil {
- . . 50: log.Fatalf("http.ListenAndServe err: %v", err)
- (pprof)
我們可以在輸出的分析中比較準(zhǔn)確的看到引起互斥鎖的函數(shù)在哪里,鎖開(kāi)銷在哪里,在本例中是第 45 行。
Block Profiling
與 Mutex 的 runtime.SetMutexProfileFraction 相似,Block 也需要調(diào)用 runtime.SetBlockProfileRate() 進(jìn)行采集量的設(shè)置,否則默認(rèn)關(guān)閉,若設(shè)置的值小于等于 0 也會(huì)認(rèn)為是關(guān)閉。
與上小節(jié) Mutex 相比,主體代碼不變,僅是新增 runtime.SetBlockProfileRate() 的調(diào)用,如下:
- func init() {
- runtime.SetBlockProfileRate(1)
- ...
- }
我們查看調(diào)用 top 命令,查看阻塞情況的排名:
- $ go tool pprof http://localhost:6061/debug/pprof/block
- Fetching profile over HTTP from http://localhost:6061/debug/pprof/block
- Saved profile in /Users/eddycjy/pprof/pprof.contentions.delay.017.pb.gz
- Type: delay
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof) top
- Showing nodes accounting for 74.54ms, 100% of 74.54ms total
- flat flat% sum% cum cum%
- 74.54ms 100% 100% 74.54ms 100% sync.(*Mutex).Lock
- 0 0% 100% 74.54ms 100% main.main.func1
同樣的,我們也可以調(diào)用 list 命令查看具體的阻塞情況,執(zhí)行方式和排查模式與先前概述的一致。
查看可視化界面
接下來(lái)我們繼續(xù)使用前面的示例程序,將其重新運(yùn)行起來(lái),然后在其它窗口執(zhí)行下述命令:
- $ wget http://127.0.0.1:6060/debug/pprof/profile
默認(rèn)需要等待 30 秒,執(zhí)行完畢后可在當(dāng)前目錄下發(fā)現(xiàn)采集的文件 profile,針對(duì)可視化界面我們有兩種方式可進(jìn)行下一步分析:
方法一(推薦):該命令將在所指定的端口號(hào)運(yùn)行一個(gè) PProf 的分析用的站點(diǎn)。
- $ go tool pprof -http=:6001 profile
方法二:通過(guò) web 命令將以 svg 的文件格式寫(xiě)入圖形,然后在 Web 瀏覽器中將其打開(kāi)。
- $ go tool pprof profile
- Type: cpu
- Time: Feb 1, 2020 at 12:09pm (CST)
- Duration: 30s, Total samples = 60ms ( 0.2%)
- Entering interactive mode (type "help" for commands, "o" for options)
- (pprof) web
如果出現(xiàn)錯(cuò)誤提示 Could not execute dot; may need to install graphviz.,那么意味著你需要安裝 graphviz組件。
另外方法一所運(yùn)行的站點(diǎn),實(shí)際上包含了方法二的內(nèi)容(svg圖片),并且更靈活,因此非特殊情況,我們會(huì)直接使用方法一的方式運(yùn)行站點(diǎn)來(lái)做觀察和分析。
剖析內(nèi)容
通過(guò) PProf 所提供的可視化界面,我們能夠更方便、更直觀的看到 Go 應(yīng)用程序的調(diào)用鏈、使用情況等。另外在 View 菜單欄中,PProf 還支持多種分析方式的切換,如下:
view 菜單欄
接下來(lái)我們將基于 CPU Profiling 所抓取的 Profile 進(jìn)行一一介紹,而其它 Profile 類型的分析模式也是互通的,只要我們了解了一種,其余的也就會(huì)了。
Top
top 欄目
該視圖與前面所講解的 top 子命令的作用和含義是一樣的,因此不再贅述。
Graph
graph 欄目
該視圖展示的為整體的函數(shù)調(diào)用流程,框越大、線越粗、框顏色越鮮艷(紅色)就代表它占用的時(shí)間越久,開(kāi)銷越大。相反若框顏色越淡,越小則代表在整體的函數(shù)調(diào)用流程中,它的開(kāi)銷是相對(duì)較小的。
因此我們可以用此視圖去分析誰(shuí)才是開(kāi)銷大頭,它又是因?yàn)槭裁凑{(diào)用流程而被調(diào)用的。
Peek
peek 欄目
此視圖相較于 Top 視圖,增加了所屬的上下文信息的展示,也就是函數(shù)的輸出調(diào)用者/被調(diào)用者。
Source
source 欄目
該視圖主要是增加了面向源代碼的追蹤和分析,可以看到其開(kāi)銷主要消耗在哪里。
Flame Graph
flame graph 概覽
Flame Graph(火焰圖)它是可動(dòng)態(tài)的,調(diào)用順序由上到下(A -> B -> C -> D),每一塊代表一個(gè)函數(shù)、顏色越鮮艷(紅)、區(qū)塊越大代表占用 CPU 的時(shí)間更長(zhǎng)。同時(shí)它也支持點(diǎn)擊塊深入進(jìn)行分析。
我們選擇頁(yè)面上的 main.main.func1 區(qū)塊,將會(huì)進(jìn)入到其屬下的下一層級(jí),如下:
進(jìn)一步查看 flame graph
這樣子我們就可以根據(jù)不同函數(shù)的多維度層級(jí)進(jìn)行分析,能夠更好的觀察其流轉(zhuǎn)并發(fā)現(xiàn)問(wèn)題。
通過(guò)測(cè)試用例做剖析
在上述章節(jié)中,我們是通過(guò)在應(yīng)用程序中埋入方法進(jìn)行采集的,那么還有一種方式,能夠更精準(zhǔn)的剖析到你所想要分析的流程或函數(shù)。
首先我們將先前所編寫(xiě)的 Add 方法挪到獨(dú)立的 package 中,命名為 add.go 文件,然后新建 add_test.go 文件,寫(xiě)入如下測(cè)試用例代碼:
- func TestAdd(t *testing.T) {
- _ = Add("go-programming-tour-book")
- }
- func BenchmarkAdd(b *testing.B) {
- for i := 0; i < b.N; i++ {
- Add("go-programming-tour-book")
- }
- }
在完成代碼編寫(xiě)后,我們回到命令行窗口執(zhí)行如下采集命令:
- $ go test -bench=. -cpuprofile=cpu.profile
執(zhí)行完畢后會(huì)在當(dāng)前命令生成 cpu.profile 文件,然后只需執(zhí)行 go tool pprof 命令就可以進(jìn)行查看了,如下圖:
cpu profile
另外除了對(duì) CPU 進(jìn)行剖析以外,我們還可以調(diào)整選項(xiàng),對(duì)內(nèi)存情況進(jìn)行分析,如下采集命令:
- $ go test -bench=. -memprofile=mem.profile
接下來(lái)和上面一樣,執(zhí)行 go tool pprof 命令進(jìn)行查看,如下圖:
進(jìn)一步查看
通過(guò) Lookup 寫(xiě)入文件做剖析
除了注入 http handler 和 go test 以外,我們還可以在程序中通過(guò) pprof 所提供的 Lookup 方法來(lái)進(jìn)行相關(guān)內(nèi)容的采集和調(diào)用。
其一共支持六種類型,分別是:
- goroutine。
- threadcreate。
- heap。
- block。
- mutex。
具體代碼如下:
- type LookupType int8
- const (
- LookupGoroutine LookupType = iota
- LookupThreadcreate
- LookupHeap
- LookupAllocs
- LookupBlock
- LookupMutex
- )
- func pprofLookup(lookupType LookupType, w io.Writer) error {
- var err error
- switch lookupType {
- case LookupGoroutine:
- p := pprof.Lookup("goroutine")
- err = p.WriteTo(w, 2)
- case LookupThreadcreate:
- p := pprof.Lookup("threadcreate")
- err = p.WriteTo(w, 2)
- case LookupHeap:
- p := pprof.Lookup("heap")
- err = p.WriteTo(w, 2)
- case LookupAllocs:
- p := pprof.Lookup("allocs")
- err = p.WriteTo(w, 2)
- case LookupBlock:
- p := pprof.Lookup("block")
- err = p.WriteTo(w, 2)
- case LookupMutex:
- p := pprof.Lookup("mutex")
- err = p.WriteTo(w, 2)
- }
- return err
- }
接下來(lái)我們只需要對(duì)該方法進(jìn)行調(diào)用就好了,其提供了 io.Writer 接口,也就是只要實(shí)現(xiàn)了對(duì)應(yīng)的 Write 方法,我們可以將其寫(xiě)到任何支持地方去,如下:
- ...
- func init() {
- runtime.SetMutexProfileFraction(1)
- runtime.SetBlockProfileRate(1)
- }
- func main() {
- http.HandleFunc("/lookup/heap", func(w http.ResponseWriter, r *http.Request) {
- _ = pprofLookup(LookupHeap, os.Stdout)
- })
- http.HandleFunc("/lookup/threadcreate", func(w http.ResponseWriter, r *http.Request) {
- _ = pprofLookup(LookupThreadcreate, os.Stdout)
- })
- http.HandleFunc("/lookup/block", func(w http.ResponseWriter, r *http.Request) {
- _ = pprofLookup(LookupBlock, os.Stdout)
- })
- http.HandleFunc("/lookup/goroutine", func(w http.ResponseWriter, r *http.Request) {
- _ = pprofLookup(LookupGoroutine, os.Stdout)
- })
- _ = http.ListenAndServe("0.0.0.0:6060", nil)
- }
在上述代碼中,我們將采集結(jié)果寫(xiě)入到了控制臺(tái)上,我們可以進(jìn)行一下驗(yàn)證,調(diào)用 http://127.0.0.1:6060/lookup/heap,控制臺(tái)輸出結(jié)果如下:
- $ go run main.go
- heap profile: 0: 0 [0: 0] @ heap/1048576
- # runtime.MemStats
- # Alloc = 180632
- # TotalAlloc = 180632
- # Sys = 69928960
- # Lookups = 0
- ...
為什么要初始化net/http/pprof
在我們的例子中,你會(huì)發(fā)現(xiàn)我們?cè)谝蒙蠈?duì) net/http/pprof 包進(jìn)行了默認(rèn)的初始化(也就是 _),如果你曾經(jīng)漏了,或者沒(méi)加,你會(huì)發(fā)現(xiàn)壓根調(diào)用不了 pprof 的相關(guān)接口,這是為什么呢,我們一起看看下面該包的初始化方法,如下:
- 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)
- }
實(shí)際上 net/http/pprof 會(huì)在初始化函數(shù)中對(duì)標(biāo)準(zhǔn)庫(kù)中 net/http 所默認(rèn)提供的 DefaultServeMux 進(jìn)行路由注冊(cè),源碼如下:
- var DefaultServeMux = &defaultServeMux
- var defaultServeMux ServeMux
- func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
- DefaultServeMux.HandleFunc(pattern, handler)
- }
而我們?cè)诶又惺褂玫?HTTP Server,也是使用的標(biāo)準(zhǔn)庫(kù)中默認(rèn)提供的,因此便完美的結(jié)合在了一起,這也恰好也是最小示例的模式。
這時(shí)候你可能會(huì)注意到另外一個(gè)問(wèn)題,那就是我們的實(shí)際項(xiàng)目中,都是有相對(duì)獨(dú)立的 ServeMux 的,這時(shí)候我們只要仿照著將 pprof 對(duì)應(yīng)的路由注冊(cè)進(jìn)去就好了,如下:
- mux := http.NewServeMux()
- mux.HandleFunc("/debug/pprof/", pprof.Index)
- mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
- mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
- mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
- mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
總結(jié)
在本文中我們?cè)敿?xì)的介紹了 Go 語(yǔ)言中 pprof 的使用,針對(duì)一些常用的套件均進(jìn)行了說(shuō)明。而 pprof 在我們平時(shí)的性能剖析,問(wèn)題排查上都占據(jù)著非常重要的角色。
在日常只需要根據(jù)合理的排查思路,相信你一定能夠根據(jù)在 pprof 中的蛛絲馬跡,解決那些或大或小的問(wèn)題,實(shí)現(xiàn)準(zhǔn)點(diǎn)下班!