快速開始
測試環境:go version go1.22.2 windows/amd64,原始碼開源在 https://github.com/oldme-git/teach-study/tree/master/golang/base/pprof
在正式開始之前,請確保安裝 graphviz
,這一步不可省略,它可以協助 pprof
生成更直觀的資料分析圖。可以參考官方網站的安裝方法。
go
使用 runtime/pprof
包來對程式進行取樣,當然,還有另外一個包 net/http/pprof
,這裡先按下不表。先來看一個 CPU 分析的例子:
package main
import (
"math"
"math/rand"
"os"
"runtime/pprof"
)
func main() {
// 儲存 CPU 取樣資料
file := "cpu.pprof"
os.Remove(file)
f, err := os.OpenFile(file, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
panic(err)
}
defer f.Close()
// 開始取樣
err = pprof.StartCPUProfile(f)
if err != nil {
panic(err)
}
defer pprof.StopCPUProfile()
// 測試程式
for i := 0; i < 1000; i++ {
nums := genRandomNumbers(10000)
for _, v := range nums {
_ = math.Pow(float64(v), rand.Float64())
}
}
}
// 測試程式,生成一個隨機數切片
func genRandomNumbers(n int) []int {
nums := make([]int, n)
for i := 1; i < n; i++ {
nums[i] = rand.Int() * n
}
return nums
}
這是一個很簡單的例子,執行 go run main.go
在當前目錄下生成一個 cpu.pprof
檔案。然後輸入命令 go tool pprof cpu.pprof
進入 pprof
的命令列中。
PS D:\project\teach-study\golang\base\pprof\cpu> go run main.go
PS D:\project\teach-study\golang\base\pprof\cpu> go tool pprof cpu.pprof
File: main.exe
Build ID: C:\Users\half\AppData\Local\Temp\go-build787417447\b001\exe\main.exe2024-05-08 11:13:12.7105156 +0800 CST
Type: cpu
Time: May 8, 2024 at 11:13am (CST)
Duration: 1.26s, Total samples = 1.07s (85.20%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
pprof
命令有很多,可以輸入 help
檢視,不過一般常用的就兩個: top
和 web
。輸入 top5
可以檢視前 5 的耗時呼叫。
// 輸入
top5
// 輸出
Showing nodes accounting for 700ms, 65.42% of 1070ms total
Showing top 5 nodes out of 69
flat flat% sum% cum cum%
210ms 19.63% 19.63% 210ms 19.63% math.archLog
180ms 16.82% 36.45% 180ms 16.82% math.archExp
160ms 14.95% 51.40% 670ms 62.62% math.pow
80ms 7.48% 58.88% 80ms 7.48% internal/chacha8rand.block
70ms 6.54% 65.42% 70ms 6.54% math/rand.globalRand
來認識一下這五個指標:
flat | 是我們最關注的指標,它代表自身耗時,不包含內部呼叫。 |
falt% | 自身耗時相對於總耗時的百分比 |
cum | 自身耗時加上內部函式呼叫的總耗時 |
cum% | 自身耗時加上內部函式呼叫的總耗時相對於總耗時的百分比 |
sum% | 前 N 行的 flat% 之和。對於上述例子的第四行 58.88=19.63+16.82+14.95+7.48 |
只是依賴文字無法很好的理解這些指標,我們可以使用 web
命令來生成更直觀的 svg
分析圖。輸入 web
命令後,會自動在瀏覽器開啟 svg
:
svg
中的每個單元格包含了包名,函式名,flat, flat%, cum, cum%
:
單元格顏色越紅,代表 cum
越大,反之越小;單元格越大,代表 flat
越大,反之越小。單元格之間的箭頭線代表呼叫鏈,線越粗代表消耗的更多的資源,反之亦然。帶有 inline
欄位表示該函式被內聯進了呼叫方(當作普通線處理就行)。
函式呼叫是存在一些固定開銷的,例如維護幀指標暫存器BP、棧溢位檢測等。因此,對於一些程式碼行比較少的函式,編譯器傾向於將它們在編譯期展開從而消除函式呼叫,這種行為就是內聯。
更多的 Web UI
透過 web
命令已經可以獲取很直觀的效能分析圖,我們還可以使用 -http
引數來啟用一個 web
服務,獲取更多的效能分析。輸入 exit
退出 pprof
命令介面,輸入命令:
go tool pprof -http=:7000 cpu.pprof
之後會自動在瀏覽器開啟 http://localhost:7000/ui/
。
在 view
中可以使用火焰圖(Flame Graph),火焰圖有新舊兩種,可以根據線的長短和顏色判斷 CPU
耗時。其他的選項可以點點看看,不復雜,很容易就學會了。
web 服務取樣
對於 web 服務的 pprof 取樣,我們可以使用基於 runtime/pprof
封裝的更便捷的 net/http/pprof
包。
package main
import (
"fmt"
"math"
"math/rand"
"net/http"
_ "net/http/pprof"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 測試程式
for i := 0; i < 1000; i++ {
nums := genRandomNumbers(10000)
for _, v := range nums {
_ = math.Pow(float64(v), rand.Float64())
}
}
fmt.Fprint(w, "Hello, world!")
})
http.ListenAndServe(":8080", nil)
}
// 測試程式,生成一個隨機數切片
func genRandomNumbers(n int) []int {
nums := make([]int, n)
for i := 1; i < n; i++ {
nums[i] = rand.Int() * n
}
return nums
}
開啟 http://127.0.0.1:8080/debug/pprof/ ,可以看到 pprof
的實時取樣資料:
這裡面一共有九個取樣資料:
allocs | 檢視歷史累計的所有記憶體分配的取樣資料 |
block | 檢視歷史累計的導致同步原語阻塞的堆疊跟蹤 |
cmdline | 包含程序的完整命令列資訊,通常用於記錄程式啟動時的命令列引數 |
goroutine | 實時檢視當前所有執行的 goroutines 堆疊跟蹤 |
heap | 實時檢視活動物件的記憶體分配情況 |
mutex | 檢視歷史累計的導致互斥鎖的競爭持有者的堆疊跟蹤 |
profile | 進行 30s 的 CPU Profiling,瀏覽器會轉圈,30s 後下載一個分析用的 profile 檔案 |
threadcreate | 檢視建立新 OS 執行緒的堆疊跟蹤 |
trace | 程式執行 trace, 和其他樣本資料不同的是,這個需要使用 go tool trace 來分析。trace 是一種更詳細的效能分析工具,用於跟蹤程式的執行過程,包括函式呼叫、協程切換等。 |
預設是不追蹤 block
和 mutex
的,如果需要,在程式碼中加入這兩個:
runtime.SetBlockProfileRate(1) // 開啟 block
runtime.SetMutexProfileFraction(1) // 開啟 mutex
這些資訊都是實時變化的,重新整理一下瀏覽器即可看見,但是這些資訊不易閱讀,我們可以把它們下載下來,使用 pprof
分析,以 allocs
為例:
// 下載 allocs 資料
curl -o allocs.pprof http://localhost:8080/debug/pprof/allocs
// pprof
go tool pprof .\allocs.pprof
非 Web 程式的其他取樣
在快速開始部分已經介紹了 CPU
取樣,對於其他取樣,可以參考這段程式碼:
package main
import (
"math"
"math/rand"
"os"
"runtime/pprof"
)
func main() {
// 儲存 CPU 取樣資料
file := "allocs.pprof"
os.Remove(file)
f, err := os.OpenFile(file, os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
panic(err)
}
defer f.Close()
// 測試程式
for i := 0; i < 1000; i++ {
nums := genRandomNumbers(10000)
for _, v := range nums {
_ = math.Pow(float64(v), rand.Float64())
}
}
pprof.Lookup("allocs").WriteTo(f, 0)
}
// 測試程式,生成一個隨機數切片
func genRandomNumbers(n int) []int {
nums := make([]int, n)
for i := 1; i < n; i++ {
nums[i] = rand.Int() * n
}
return nums
}