golang?pprof?監(jiān)控goroutine?thread統(tǒng)計(jì)原理詳解
引言
在之前 golang pprof監(jiān)控 系列文章里我分別介紹了go trace以及go pprof工具對memory,block,mutex這些維度的統(tǒng)計(jì)原理,今天我們接著來介紹golang pprof工具對于goroutine 和thread的統(tǒng)計(jì)原理。
還記得在golang pprof監(jiān)控系列 memory,block,mutex 使用 文章里,通過http接口的方式暴露的方式展現(xiàn) 指標(biāo)信息那個(gè)網(wǎng)頁圖嗎?
這一節(jié),我將會(huì)介紹其中的goroutine部分和threadcreate部分。
老規(guī)矩,在介紹統(tǒng)計(jì)原理前,先來看看http接口暴露的方式暴露了哪些信息。
http 接口暴露的方式
讓我們點(diǎn)擊網(wǎng)頁的goroutine 鏈接。。。
goroutine profile 輸出信息介紹
進(jìn)入到了一個(gè)這樣的界面,我們挨個(gè)分析下網(wǎng)頁展現(xiàn)出來的信息:
首先地址欄 /debug/pprof/goroutine?debug= 1 代表這是在訪問goroutine指標(biāo)信息,debug =1 代表訪問的內(nèi)容將會(huì)以文本可讀的形式展現(xiàn)出來。 debug=0 則是會(huì)下載一個(gè)goroutine指標(biāo)信息的二進(jìn)制文件,這個(gè)文件可以通過go tool pprof 工具去進(jìn)行分析,關(guān)于go tool pprof 的使用網(wǎng)上也有相當(dāng)多的資料,這里就不展開了。 debug = 2 將會(huì)把當(dāng)前所有協(xié)程的堆棧信息以文本可讀形式展示在網(wǎng)頁上。如下圖所示:
debug =2 時(shí)的 如上圖所示,41代表協(xié)程的id,方括號內(nèi)running代表了協(xié)程的狀態(tài)是運(yùn)行中,接著就是該協(xié)程此時(shí)的堆棧信息了。
讓我們再回到debug = 1的分析上面去,剛才分析完了地址欄里的debug參數(shù),接著,我們看輸出的第一行
goroutine profile: total 6 1 @ 0x102ad6c60 0x102acf7f4 0x102b04de0 0x102b6e850 0x102b6e8dc 0x102b6f79c 0x102c27d04 0x102c377c8 0x102d0fc74 0x102bea72c 0x102bebec0 0x102bebf4c 0x102ca4af0 0x102ca49dc 0x102d0b084 0x102d10f30 0x102d176a4 0x102b09fc4 # 0x102b04ddf internal/poll.runtime_pollWait+0x5f /Users/xiongchuanhong/goproject/src/go/src/runtime/netpoll.go:303 # 0x102b6e84f internal/poll.(*pollDesc).wait+0x8f /Users/xiongchuanhong/goproject/src/go/src/internal/poll/fd_poll_runtime.go:84 ......
goroutine profile 表明了這個(gè)profile的類型。
total 6 代表此時(shí)一共有6個(gè)協(xié)程。
接著是下面一行,1 代表了在這個(gè)堆棧上,只有一個(gè)協(xié)程在執(zhí)行。但其實(shí)在計(jì)算出數(shù)字1時(shí),并不僅僅按堆棧去做區(qū)分,還依據(jù)了協(xié)程labels值,也就是 協(xié)程的堆棧和lebels標(biāo)簽值 共同構(gòu)成了一個(gè)key,而數(shù)字1就是在遍歷所有協(xié)程信息時(shí),對相同key進(jìn)行累加計(jì)數(shù)得來的。
我們可以通過下面的方式為協(xié)程設(shè)置labels。
pprof.SetGoroutineLabels(pprof.WithLabels(context.Background(), pprof.Labels("name", "lanpangzi", "age", "18")))
通過上述代碼,我可以為當(dāng)前協(xié)程設(shè)置了兩個(gè)標(biāo)簽值,分別是name和age,設(shè)置label值之后,再來看debug=1后的網(wǎng)頁輸出,可以發(fā)現(xiàn) 設(shè)置的labels出現(xiàn)了。
1 @ 0x104f86c60 0x104fb7358 0x105236368 0x104f867ec 0x104fba024 # labels: {"age":"18", "name":"lanpangzi"} # 0x104fb7357 time.Sleep+0x137 /Users/xiongchuanhong/goproject/src/go/src/runtime/time.go:193 # 0x105236367 main.main+0x437 /Users/xiongchuanhong/goproject/src/go/main/main.go:46 # 0x104f867eb runtime.main+0x25b /Users/xiongchuanhong/goproject/src/go/src/runtime/proc.go:255
而數(shù)字1之后,就是協(xié)程正在執(zhí)行的堆棧信息了。至此,goroutine指標(biāo)的輸出信息介紹完畢。
threadcreate 輸出信息介紹
介紹完goroutine指標(biāo)的輸出信息后,再來看看threadcreate 線程創(chuàng)建指標(biāo)的 輸出信息。
老規(guī)矩,先看地址欄,debug=1代表 輸出的是文本可讀的信息,threadcreate 就沒有debug=2的特別輸出了,debug=0時(shí) 同樣也會(huì)下載一個(gè)可供go tool pprof分析的二進(jìn)制文件。
接著threadcreate pfofile表明了profile的類型, total 12 代表了此時(shí)總共有12個(gè)線程被創(chuàng)建,然后緊接著是11 代表了在這個(gè)總共有11個(gè)線程是在這個(gè)堆棧的代碼段上被創(chuàng)建的,注意這里后面沒有堆棧內(nèi)容,說明runtime在創(chuàng)建線程時(shí),并沒有把此時(shí)的堆棧記錄下來,原因有可能是 這個(gè)線程是runtime自己使用的,堆棧沒有必要展示給用戶,所以干脆不記錄了,具體原因這里就不深入研究了。
下面輸出的內(nèi)容可以看到在main方法里面創(chuàng)建了一個(gè)線程,runtime.newm 方法內(nèi)部,runtime會(huì)啟動(dòng)一個(gè)系統(tǒng)線程。
threadcreate 輸出內(nèi)容比較簡單,沒有過多可以講的。
程序代碼暴露指標(biāo)信息
看完了http接口暴露著兩類指標(biāo)的方式,我們再來看看如何通過代碼來暴露他們。 還記得在golang pprof監(jiān)控系列memory,block,mutex 使用 是如何通過程序代碼 暴露memory block mutex 指標(biāo)的嗎,goroutine 和 threadcreate 和他們一樣,也是通過pprof.Lookup方法進(jìn)行暴露的。
os.Remove("goroutine.out") f, _ := os.Create("goroutine.out") defer f.Close() err := pprof.Lookup("goroutine").WriteTo(f, 1) if err != nil { log.Fatal(err) } .... os.Remove("threadcreate.out") f, _ := os.Create("threadcreate.out") defer f.Close() err := pprof.Lookup("threadcreate").WriteTo(f, 1) if err != nil { log.Fatal(err) }
無非就是將pprof.Lookup的傳入的參數(shù)值改成對應(yīng)的指標(biāo)名即可。
接著我們來看看runtime內(nèi)部是如何對這兩種類型的指標(biāo)進(jìn)行統(tǒng)計(jì)的,好的,正戲開始。
統(tǒng)計(jì)原理介紹
無論是 goroutine 還是threadcreate 的指標(biāo)信息的輸出,都是調(diào)用了同一個(gè)方法writeRuntimeProfile。 golang 源碼版本 go1.17.12。
// src/runtime/pprof/pprof.go:708 func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]runtime.StackRecord, []unsafe.Pointer) (int, bool)) error { var p []runtime.StackRecord var labels []unsafe.Pointer n, ok := fetch(nil, nil) for { p = make([]runtime.StackRecord, n+10) labels = make([]unsafe.Pointer, n+10) n, ok = fetch(p, labels) if ok { p = p[0:n] break } } return printCountProfile(w, debug, name, &runtimeProfile{p, labels}) }
讓我們來分析下這個(gè)函數(shù),函數(shù)會(huì)傳遞一個(gè)fetch 方法,goroutine和threadcreate信息在輸出時(shí)選擇了不同的fetch方法來獲取到各自的信息。
為了對主干代碼有比較清晰的認(rèn)識,先暫時(shí)不看fetch方法的具體實(shí)現(xiàn),此時(shí)我們只需要知道,fetch方法可以將需要的指標(biāo)信息 獲取到,并且將信息的堆棧存到變量名為p的堆棧類型的切片里,然后將labels信息,存儲到 變量名為labels的切片里。
注意: 只有g(shù)oroutine類型的指標(biāo)才有l(wèi)abels信息
獲取到了堆棧信息,labels 信息,接著就是要將這些信息進(jìn)行輸出了,進(jìn)行輸出的函數(shù)是 上述源碼里的最后一行 中 的printCountProfile 函數(shù)。
printCountProfile 函數(shù)的邏輯比較簡單,我簡單概括下,輸出的時(shí)候會(huì)將 printCountProfile 參數(shù)中的堆棧信息連同labels構(gòu)成的結(jié)構(gòu)體 進(jìn)行遍歷, 堆棧信息和labels信息組合作為key,對相同key的內(nèi)容進(jìn)行累加計(jì)數(shù)。最后 printCountProfile 將根據(jù)debug的值的不同選擇不同的輸出方式,例如debug=0是二進(jìn)制文件下載 方式 ,debug=1則是 網(wǎng)頁文本可讀方式進(jìn)行輸出
至此,對goroutine和threadcreate 指標(biāo)信息的輸出過程應(yīng)該有了解了,即通過fetch方法獲取到指標(biāo)信息,然后通過printCountProfile 方法對指標(biāo)信息進(jìn)行輸出。
fetch 方法的具體實(shí)現(xiàn),我們還沒有開始介紹,現(xiàn)在來看看,goroutine和threadcreate信息在輸出時(shí)選擇了不同的fetch方法來獲取到各自的信息。
源碼如下:
// src/runtime/pprof/pprof.go:661 func writeThreadCreate(w io.Writer, debug int) error { return writeRuntimeProfile(w, debug, "threadcreate", func(p []runtime.StackRecord, _ []unsafe.Pointer) (n int, ok bool) { return runtime.ThreadCreateProfile(p) }) } // src/runtime/pprof/pprof.go:680 func writeGoroutine(w io.Writer, debug int) error { if debug >= 2 { return writeGoroutineStacks(w) } return writeRuntimeProfile(w, debug, "goroutine", runtime_goroutineProfileWithLabels) }
goroutine 指標(biāo)信息在輸出時(shí),會(huì)選擇runtime_goroutineProfileWithLabels函數(shù)來獲取goroutine指標(biāo),而threadcreate 則會(huì)調(diào)用 runtime.ThreadCreateProfile(p) 去獲取threadcreate指標(biāo)信息。
goroutine fetch 函數(shù)實(shí)現(xiàn)
runtime_goroutineProfileWithLabels 方法的實(shí)現(xiàn)是由go:linkname 標(biāo)簽鏈接過去的,實(shí)際底層實(shí)現(xiàn)的方法是 runtime_goroutineProfileWithLabels。
// src/runtime/mprof.go:744 //go:linkname runtime_goroutineProfileWithLabels runtime/pprof.runtime_goroutineProfileWithLabels func runtime_goroutineProfileWithLabels(p []StackRecord, labels []unsafe.Pointer) (n int, ok bool) { return goroutineProfileWithLabels(p, labels) }
goroutineProfileWithLabels 就是實(shí)際獲取goroutine堆棧和標(biāo)簽的方法了。
我們往goroutineProfileWithLabels 傳遞了兩個(gè)數(shù)組,分別用于存儲堆棧信息,和labels信息,而goroutineProfileWithLabels 則負(fù)責(zé)將兩個(gè)數(shù)組填充上對應(yīng)的信息。
goroutineProfileWithLabels 的邏輯也比較容易,我這里僅僅簡單概括下,其內(nèi)部會(huì)通過一個(gè)全局變量allgptr 去遍歷所有的協(xié)程,allgptr 保存了程序中所有的協(xié)程的地址, 而協(xié)程的結(jié)構(gòu)體g內(nèi)部,有一個(gè)叫做label的屬性,這個(gè)值就代表協(xié)程的標(biāo)簽值,在遍歷協(xié)程時(shí),通過該屬性便可以獲取到標(biāo)簽值了。
threadcreate fetch 函數(shù)實(shí)現(xiàn)
runtime.ThreadCreateProfile 是 獲取threadcreate 指標(biāo)的方法。
源碼如下:
func ThreadCreateProfile(p []StackRecord) (n int, ok bool) { first := (*m)(atomic.Loadp(unsafe.Pointer(&allm))) for mp := first; mp != nil; mp = mp.alllink { n++ } if n <= len(p) { ok = true i := 0 for mp := first; mp != nil; mp = mp.alllink { p[i].Stack0 = mp.createstack i++ } } return }
首先是獲取到allm變量的地址,allm是一個(gè)全局變量,它其實(shí)是 存儲所有m鏈表 的表頭元素。
// src/runtime/runtime2.go:1092 var ( allm *m .....
在golang里,每創(chuàng)建一個(gè)m結(jié)構(gòu)便會(huì)在底層創(chuàng)建一個(gè)系統(tǒng)線程,所以你可以簡單的認(rèn)為m就是代表了一個(gè)線程??梢灾笊钊肓私庀耮pm模型。
for mp := first; mp != nil; mp = mp.alllink { p[i].Stack0 = mp.createstack i++ }
然后 ThreadCreateProfile 里 這段邏輯就是遍歷了整個(gè)m鏈表,將m結(jié)構(gòu)體保存的堆棧信息賦值給 參數(shù)p,p則是我們需要填充的堆棧信息數(shù)組,在m結(jié)構(gòu)體里,alllink是一個(gè)指向鏈表下一個(gè)元素的指針,每次新創(chuàng)建m時(shí),會(huì)將新m插入到表頭位置,然后更新allm變量。
總結(jié)
至此,goroutine 和threadcreate的使用和原理都介紹完了,他們比起之前的memory,block之類的統(tǒng)計(jì)相對來說比較簡單,簡而言之就是遍歷一個(gè)全局變量allgptr或者allm ,遍歷時(shí)獲取到協(xié)程或者線程的堆棧信息和labels信息,然后將這些信息進(jìn)行輸出即可。
以上就是golang pprof 監(jiān)控goroutine thread統(tǒng)計(jì)原理詳解的詳細(xì)內(nèi)容,更多關(guān)于go pprof goroutine thread統(tǒng)計(jì)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang優(yōu)化目錄遍歷的實(shí)現(xiàn)方法
對于go1.16的新變化,大家印象最深的可能是io包的大規(guī)模重構(gòu),但這個(gè)重構(gòu)實(shí)際上還引進(jìn)了一個(gè)優(yōu)化,這篇文章要說的就是這個(gè)優(yōu)化,所以本將給大家介紹golang是如何優(yōu)化目錄遍歷的,需要的朋友可以參考下2024-08-08Go語言關(guān)于幾種深度拷貝(deepcopy)方法的性能對比
這篇文章主要介紹了Go語言關(guān)于幾種深度拷貝(deepcopy)方法的性能對比,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01go微服務(wù)PolarisMesh源碼解析服務(wù)端啟動(dòng)流程
這篇文章主要為大家介紹了go微服務(wù)PolarisMesh源碼解析服務(wù)端啟動(dòng)流程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01Go Slice進(jìn)行參數(shù)傳遞如何實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Go Slice進(jìn)行參數(shù)傳遞如何實(shí)現(xiàn)的過程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏的解決方案詳解
這篇文章主要為大家詳細(xì)介紹了當(dāng)Golang因Channel未關(guān)閉導(dǎo)致內(nèi)存泄漏時(shí)蓋如何解決,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2023-07-07使用Go和Gorm實(shí)現(xiàn)讀取SQLCipher加密數(shù)據(jù)庫
本文檔主要描述通過Go和Gorm實(shí)現(xiàn)生成和讀取SQLCipher加密數(shù)據(jù)庫以及其中踩的一些坑,文章通過代碼示例講解的非常詳細(xì), 對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-06-06GoFrame通用類型變量gvar與interface基本使用對比
這篇文章主要為大家介紹了GoFrame通用類型變量gvar與interface基本使用對比,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06