聊聊Golang性能分析工具pprof的使用
對(duì)于線上穩(wěn)定運(yùn)行的服務(wù)來說, 可能會(huì)遇到 cpu、mem 利用率升高的問題。有時(shí)可能增加機(jī)器配置或重啟服務(wù)也解決不了問題。那我們就需要使用 pprof 工具來進(jìn)行性能分析, 找出造成 cpu、mem 升高的問題。
pprof
pprof 是 golang 的性能分析工具。pprof 通過讀取 profile.proto 格式的性能采樣數(shù)據(jù)來生成相關(guān)報(bào)告。從而分析服務(wù)中存在的性能問題。
pprof profiles
存儲(chǔ)采樣的數(shù)據(jù)集合。在 golang 中, 采樣數(shù)據(jù)分別包含這幾種類型:
cpu
cpu 采樣是最常用的采用類型。當(dāng)開始 cpu 采樣時(shí), 每隔 10ms 會(huì)進(jìn)行一次采樣, 記錄當(dāng)前運(yùn)行 goroutines 的堆棧信息。當(dāng)采樣完成之后, 可以分析哪些代碼比較消耗 cpu。
memory
memory 采樣主要用來分析服務(wù)的堆內(nèi)存的分配和回收。程序運(yùn)行時(shí), 棧內(nèi)存是可以直接分配和回收的。因此, 無需采集棧內(nèi)存的使用情況。
block
采集阻塞數(shù)據(jù)。主要用于定位 channel 所導(dǎo)致的 goroutines 阻塞。
mutex
主要用來采集鎖競爭相關(guān)數(shù)據(jù), 分析鎖競爭導(dǎo)致的系統(tǒng)的延遲。
生成 profile 文件
在開始進(jìn)行性能相關(guān)分析之前, 需要生成對(duì)應(yīng)的采樣文件。然后通過 pprof 工具對(duì) profile 文件進(jìn)行分析, 找出影響性能的代碼。在 golang 中, 可以使用以下三種方式生成 profile 文件:
測試函數(shù)
在使用基準(zhǔn)測試時(shí), 可以指定參數(shù)生成對(duì)應(yīng)的 profile 文件。
go test -cpuprofile {proflie_name} -bench .
http 訪問
對(duì)于持續(xù)運(yùn)行的任務(wù), 例如:http、rpc??梢詫?dǎo)入 net/http/pprof 來注冊 debug 路由, 生成對(duì)應(yīng)的 proflie 文件。對(duì)于非 http 的服務(wù), 需要注冊一個(gè) debug 端口。
直接生成 profile 文件
對(duì)于非持續(xù)性任務(wù)。例如:定時(shí)任務(wù)。可以通過指定方法生成對(duì)應(yīng)的 proflie 文件。
import ( "github.com/pkg/profile" // 對(duì) runtime/pprof 進(jìn)行的封裝 "fmt" ) func main() { defer func() { profile.Start(profile.CPUProfile, profile.ProfilePath(".")) }() func() { fmt.Println(5 + 6) }() }
分析 profile 文件
下面將通過具體的例子, 來嘗試分析對(duì)應(yīng)的 cpu、mem 等指標(biāo)。
cpu 分析
在下面的例子中, 嘗試分析哪個(gè)函數(shù)比較占用 cpu.
package main import ( "log" "net/http" _ "net/http/pprof" "runtime" "time" ) //go:noinline func req(num int64) { A(num) B(num) } //go:noinline func B(num int64) { for i := int64(0); i < num/2; i++ { } } //go:noinline func A(num int64) { for i := int64(0); i < num; i++ { } } func main() { runtime.GOMAXPROCS(1) go func() { log.Println(http.ListenAndServe("localhost:3016", nil)) }() for { time.Sleep(time.Second / 4) go func() { req(time.Now().Unix()) }() } }
下載并保存對(duì)應(yīng)的 cpu profile, 采樣時(shí)間 19s:
curl ``http://127.0.0.1:3016/debug/pprof/profile``?seconds=19 --output cpu.pprof
啟動(dòng)可視化界面:
go tool pprof -http=:8080 cpu.pprof
從最右側(cè)可以看出, 此次采集共耗時(shí) 19.21s, 采樣得到的 cpu 時(shí)間為 16.73s 「87.10%」。由于是采樣, 所以 total samples 是小于 Duration 的。通過 view 來切換不同的試圖。
Top, 用于展示不同函數(shù)的耗時(shí)占比。Flat: 當(dāng)前函數(shù)運(yùn)行耗時(shí)。Flat%: Flat 對(duì)應(yīng)的占比。Sum%: 累積使用占比。Cum: 當(dāng)前函數(shù)及子函數(shù)運(yùn)行耗時(shí)。Cum%: Cum 對(duì)應(yīng)的占比。
對(duì)于下面的例子:統(tǒng)計(jì)到的 cpu 時(shí)間為 16.73s, 函數(shù) A 本身耗時(shí) 13.3s 「79.49%」; 函數(shù) A 及其子函數(shù)耗時(shí)為 13.85s「82.78%」; 函數(shù) req 本身耗時(shí) 0; 子函數(shù)耗時(shí) 16.73s「100%」。
Graph, 以圖的形式展示不同函數(shù)的耗時(shí)占比。如下圖所示:函數(shù)入口為 main.func2「匿名函數(shù)」; 0 of 16730ms「匿名函數(shù)不占用任何資源, cpu 采樣總時(shí)長為 16730ms」; main.req 為 main.func2 的子函數(shù)「函數(shù)自身不占用任何資源」; main.A「13850ms 函數(shù)自身占用 13300ms」 和 mian.B「2880ms 函數(shù)自身占用 2810ms」 為 main.req「16730ms」 的子函數(shù)。
通過 Graph 可以直觀的看到哪些函數(shù)占用的資源比較高, 方框越大資源占比越高。同時(shí)可以直觀的看到調(diào)用關(guān)系。
Flame Graph, 火焰圖。通過火焰圖可以看到各個(gè)函數(shù)的耗時(shí), 以及子函數(shù)的耗時(shí)。橫軸越長占用的資源越多, 縱軸越長, 調(diào)用層級(jí)越深。
Source, 查看對(duì)應(yīng)的源碼。在下圖中, req 的子函數(shù) A 和 B 共占用了 100% 的資源。在函數(shù) B 中, 函數(shù)自身耗時(shí)占用了 2.81s, 其中 for 循環(huán)語句占用 2.18s。
mem 分析
下面是一段內(nèi)存分配的代碼。heap 采樣并不需要像 cpu 采樣一樣確定固定周期。heap 關(guān)注的是內(nèi)存的分配和回收, 只需采集對(duì)應(yīng)的事件即可。
package main import ( "fmt" "log" "net/http" _ "net/http/pprof" "runtime" "time" ) //go:noinline func req() { buf = append(buf, allocate()...) } //go:noinline func allocate() []byte { var b []byte for i := 0; i < 1024; i++ { temp := make([]byte, 1024) b = append(b, temp...) } return b } var buf []byte func main() { runtime.GOMAXPROCS(1) go func() { log.Println(http.ListenAndServe("localhost:3016", nil)) }() for { time.Sleep(time.Second) go func() { req() fmt.Println(len(buf)) }() } }
下載堆內(nèi)存采樣文件。
curl ``http://127.0.0.1:3016/debug/pprof/heap`` --output mem.pprof
使用 webui 打開文件。
go tool pprof -http=:8080 mem.pprof
共分為四種采樣類型:alloc_objects: 所有分配的對(duì)象; alloc_space: 所有分配的空間; inuse_objects: 活躍的對(duì)象; inuse_space: 活躍的空間。這里需要注意的是, inuse_space 使用的空間是要小于當(dāng)前進(jìn)程使用的系統(tǒng)空間的「使用中的, gc 未釋放給操作系統(tǒng)的, cgo 申請(qǐng)的空間」
通過 alloc 可以分析哪里在頻繁的分配空間, inuse 可以用來分析內(nèi)存沒有釋放。切到 inuse_sapce, 可以看到 req 函數(shù)以及它的子函數(shù) allocate 在不斷分配空間。
goroutine 分析
下面是一個(gè) goroutine 泄漏的例子。
package main import ( "fmt" "log" "net/http" _ "net/http/pprof" "runtime" "time" ) //go:noinline func req() { c := make(chan struct{}) tick := time.NewTicker(time.Millisecond) go func() { time.Sleep(time.Millisecond * 2) c <- struct{}{} }() select { case <-c: return case <-tick.C: return } } func main() { runtime.GOMAXPROCS(1) go func() { log.Println(http.ListenAndServe("localhost:3016", nil)) }() for { time.Sleep(time.Second / 10) go func() { req() }() fmt.Println(runtime.NumGoroutine()) } }
下載堆 goroutine 采樣文件。
curl ``http://127.0.0.1:3016/debug/pprof/``goroutine --output goroutine.pprof
使用 webui 打開文件。
go tool pprof -http=:8080 goroutine.pprof
切到火焰圖, 可以看到 main.req.func1「匿名函數(shù)」占用了需要 goroutine。我們切到 Souce 定位到代碼行數(shù)??梢钥吹? 卡在了往非緩存的 channel 寫數(shù)據(jù)。由于接收端已經(jīng) return, 以此 goroutine 等待寫入而無法釋放。我們只需聲明一個(gè)帶緩沖的 channel 就可以解決問題。
總結(jié)
本文簡單介紹了 golang 的性能分析工具。如何進(jìn)行性能采樣, 查看采樣的數(shù)據(jù)并對(duì)其進(jìn)行分析。介紹了 Graph、Flame Graph、Source、Top 的用法。最后, 通過幾個(gè)簡單的例子來進(jìn)行 cpu、mem、goroutine 的分析。
其實(shí), 在真實(shí)的的線上服務(wù)中, 如果沒有遇到具體的問題「接口變慢、cpu 或 mem 持續(xù)升高、goroutine 泄漏、死鎖」, 不太建議過度的進(jìn)行優(yōu)化。倒不如花精力聚焦在業(yè)務(wù)思考上。
以上就是聊聊Golang性能分析工具pprof的使用的詳細(xì)內(nèi)容,更多關(guān)于Golang pprof的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go實(shí)現(xiàn)服務(wù)優(yōu)雅關(guān)閉的示例
本文主要介紹了go實(shí)現(xiàn)服務(wù)優(yōu)雅關(guān)閉的示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02GoFrame框架gredis優(yōu)雅的取值和類型轉(zhuǎn)換
這篇文章主要為大家介紹了GoFrame框架gredis優(yōu)雅的取值和類型轉(zhuǎn)換,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06使用Golang實(shí)現(xiàn)WebSocket心跳機(jī)制
WebSocket是一種在客戶端和服務(wù)器之間實(shí)現(xiàn)全雙工通信的協(xié)議,它允許實(shí)時(shí)地傳輸數(shù)據(jù),并且比傳統(tǒng)的HTTP請(qǐng)求更加高效,在使用Golang構(gòu)建WebSocket應(yīng)用程序時(shí),一個(gè)重要的考慮因素是如何實(shí)現(xiàn)心跳機(jī)制,所以本文將探討如何使用Golang實(shí)現(xiàn)WebSocket心跳2023-11-11