聊聊Golang性能分析工具pprof的使用
對(duì)于線(xiàn)上穩(wěn)定運(yùn)行的服務(wù)來(lái)說(shuō), 可能會(huì)遇到 cpu、mem 利用率升高的問(wèn)題。有時(shí)可能增加機(jī)器配置或重啟服務(wù)也解決不了問(wèn)題。那我們就需要使用 pprof 工具來(lái)進(jìn)行性能分析, 找出造成 cpu、mem 升高的問(wèn)題。
pprof
pprof 是 golang 的性能分析工具。pprof 通過(guò)讀取 profile.proto 格式的性能采樣數(shù)據(jù)來(lái)生成相關(guān)報(bào)告。從而分析服務(wù)中存在的性能問(wèn)題。
pprof profiles
存儲(chǔ)采樣的數(shù)據(jù)集合。在 golang 中, 采樣數(shù)據(jù)分別包含這幾種類(lèi)型:
cpu
cpu 采樣是最常用的采用類(lèi)型。當(dāng)開(kāi)始 cpu 采樣時(shí), 每隔 10ms 會(huì)進(jìn)行一次采樣, 記錄當(dāng)前運(yùn)行 goroutines 的堆棧信息。當(dāng)采樣完成之后, 可以分析哪些代碼比較消耗 cpu。
memory
memory 采樣主要用來(lái)分析服務(wù)的堆內(nèi)存的分配和回收。程序運(yùn)行時(shí), 棧內(nèi)存是可以直接分配和回收的。因此, 無(wú)需采集棧內(nèi)存的使用情況。
block
采集阻塞數(shù)據(jù)。主要用于定位 channel 所導(dǎo)致的 goroutines 阻塞。
mutex
主要用來(lái)采集鎖競(jìng)爭(zhēng)相關(guān)數(shù)據(jù), 分析鎖競(jìng)爭(zhēng)導(dǎo)致的系統(tǒng)的延遲。
生成 profile 文件
在開(kāi)始進(jìn)行性能相關(guān)分析之前, 需要生成對(duì)應(yīng)的采樣文件。然后通過(guò) pprof 工具對(duì) profile 文件進(jìn)行分析, 找出影響性能的代碼。在 golang 中, 可以使用以下三種方式生成 profile 文件:
測(cè)試函數(shù)
在使用基準(zhǔn)測(cè)試時(shí), 可以指定參數(shù)生成對(duì)應(yīng)的 profile 文件。
go test -cpuprofile {proflie_name} -bench .
http 訪問(wèn)
對(duì)于持續(xù)運(yùn)行的任務(wù), 例如:http、rpc??梢詫?dǎo)入 net/http/pprof 來(lái)注冊(cè) debug 路由, 生成對(duì)應(yīng)的 proflie 文件。對(duì)于非 http 的服務(wù), 需要注冊(cè)一個(gè) debug 端口。
直接生成 profile 文件
對(duì)于非持續(xù)性任務(wù)。例如:定時(shí)任務(wù)??梢酝ㄟ^(guò)指定方法生成對(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 文件
下面將通過(guò)具體的例子, 來(lái)嘗試分析對(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 的。通過(guò) view 來(lái)切換不同的試圖。
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í)長(zhǎng)為 16730ms」; main.req 為 main.func2 的子函數(shù)「函數(shù)自身不占用任何資源」; main.A「13850ms 函數(shù)自身占用 13300ms」 和 mian.B「2880ms 函數(shù)自身占用 2810ms」 為 main.req「16730ms」 的子函數(shù)。
通過(guò) Graph 可以直觀的看到哪些函數(shù)占用的資源比較高, 方框越大資源占比越高。同時(shí)可以直觀的看到調(diào)用關(guān)系。
Flame Graph, 火焰圖。通過(guò)火焰圖可以看到各個(gè)函數(shù)的耗時(shí), 以及子函數(shù)的耗時(shí)。橫軸越長(zhǎng)占用的資源越多, 縱軸越長(zhǎng), 調(diào)用層級(jí)越深。
Source, 查看對(duì)應(yīng)的源碼。在下圖中, req 的子函數(shù) A 和 B 共占用了 100% 的資源。在函數(shù) B 中, 函數(shù)自身耗時(shí)占用了 2.81s, 其中 for 循環(huán)語(yǔ)句占用 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 打開(kāi)文件。
go tool pprof -http=:8080 mem.pprof
共分為四種采樣類(lèi)型:alloc_objects: 所有分配的對(duì)象; alloc_space: 所有分配的空間; inuse_objects: 活躍的對(duì)象; inuse_space: 活躍的空間。這里需要注意的是, inuse_space 使用的空間是要小于當(dāng)前進(jìn)程使用的系統(tǒng)空間的「使用中的, gc 未釋放給操作系統(tǒng)的, cgo 申請(qǐng)的空間」
通過(guò) alloc 可以分析哪里在頻繁的分配空間, inuse 可以用來(lái)分析內(nèi)存沒(mé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 打開(kāi)文件。
go tool pprof -http=:8080 goroutine.pprof
切到火焰圖, 可以看到 main.req.func1「匿名函數(shù)」占用了需要 goroutine。我們切到 Souce 定位到代碼行數(shù)??梢钥吹? 卡在了往非緩存的 channel 寫(xiě)數(shù)據(jù)。由于接收端已經(jīng) return, 以此 goroutine 等待寫(xiě)入而無(wú)法釋放。我們只需聲明一個(gè)帶緩沖的 channel 就可以解決問(wèn)題。
總結(jié)
本文簡(jiǎn)單介紹了 golang 的性能分析工具。如何進(jìn)行性能采樣, 查看采樣的數(shù)據(jù)并對(duì)其進(jìn)行分析。介紹了 Graph、Flame Graph、Source、Top 的用法。最后, 通過(guò)幾個(gè)簡(jiǎn)單的例子來(lái)進(jìn)行 cpu、mem、goroutine 的分析。
其實(shí), 在真實(shí)的的線(xiàn)上服務(wù)中, 如果沒(méi)有遇到具體的問(wèn)題「接口變慢、cpu 或 mem 持續(xù)升高、goroutine 泄漏、死鎖」, 不太建議過(guò)度的進(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)閉的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02GoFrame框架gredis優(yōu)雅的取值和類(lèi)型轉(zhuǎn)換
這篇文章主要為大家介紹了GoFrame框架gredis優(yōu)雅的取值和類(lèi)型轉(zhuǎn)換,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06Go語(yǔ)言封裝HTTP請(qǐng)求的Curl工具包詳解
在 Go 語(yǔ)言開(kāi)發(fā)中,與 HTTP 服務(wù)進(jìn)行交互是非常常見(jiàn)的需求,本文將分享一個(gè)用 Go 語(yǔ)言封裝的 Curl 工具包,它提供了簡(jiǎn)潔易用的接口來(lái)進(jìn)行 HTTP 請(qǐng)求,需要的可以了解下2025-03-03Go語(yǔ)言實(shí)現(xiàn)Sm2加解密的示例代碼
本文主要介紹了Go語(yǔ)言實(shí)現(xiàn)Sm2加解密的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Go語(yǔ)言官方依賴(lài)注入工具Wire的使用教程
依賴(lài)注入是一種實(shí)現(xiàn)控制反轉(zhuǎn)且用于解決依賴(lài)性問(wèn)題的設(shè)計(jì)模式。Golang?中常用的依賴(lài)注入工具主要有?Inject?、Dig?等。但是今天主要介紹的是?Go?團(tuán)隊(duì)開(kāi)發(fā)的?Wire,一個(gè)編譯期實(shí)現(xiàn)依賴(lài)注入的工具,感興趣的可以了解一下2022-09-09GO語(yǔ)言 復(fù)合類(lèi)型專(zhuān)題
這篇文章主要介紹了GO語(yǔ)言 復(fù)合類(lèi)型的的相關(guān)資料,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-06-06使用Golang實(shí)現(xiàn)WebSocket心跳機(jī)制
WebSocket是一種在客戶(hù)端和服務(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