什麼是 Profile?
在計算機性能調試領域裏,profile 就是對應用的畫像,這裏畫像就是應用使用 CPU 和內存等情況,也就是說應用使用了多少 CPU 資源、都是哪些部分在使用、每個函數使用的比例是多少、有哪些函數在等待 CPU 資源等等。知道了這些,我們就能對應用進行規劃,也能快速定位性能瓶頸。
Golang 是一個對性能特別看重的語言,因此語言中自帶了 profile 的庫,這篇文章就要講解怎麼在 golang 中做 profile。
在 Golang 中,主要關注的應用運行情況主要包括以下幾種:
- CPU profile:報告程序的 CPU 使用情況,按照一定頻率去採集應用程序在 CPU 和寄存器上面的數據
- Memory profile(Heap profile):報告程序的內存使用情況
- Block profile:報告 goroutines 不在運行狀態的情況,可以用來分析和查找死鎖等性能瓶頸
- Goroutine profile:報告 goroutines 的使用情況,有哪些 goroutine,它們的調用關係是怎樣的
兩種收集方式
分析 profile 第一步就是怎麼獲取應用程序的運行情況數據。Golang 提供了 runtime/pprof
和 net/http/pprof
兩個庫,分別應用於兩種不同的應用。
工具型應用
如果你的應用是一次性的,運行一段時間就結束,那麼最好的辦法就是在應用退出時把 profile 的報告保存到文件中,進行分析。對於這種情況,可以使用 runtime/pprof
庫。
pprof
封裝了很好的接口供我們使用,比如要想進行 CPU profile,可以調用 pprof.StartCPUProfile()
方法,它會對當前應用程序進行 CPU profile,並寫入到提供的參數中(w io.Writer
)。要停止寫入,調用 StopCPUProfile()
即可。
去除錯誤處理只需要三行內容,一般把它們寫在 main.go
文件中,應用程序啓動之後就開始執行:
f, err := os.Create(*cpuprofile)
...
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
應用執行結束後,就會生成一個文件,保存了我們的 CPU profile 數據。
要獲得內存的數據,直接使用 WriteHeapProfile
即可,不用 start
和 stop
這兩個步驟:
f, err := os.Create(*memprofile)
pprof.WriteHeapProfile(f)
f.Close()
服務型應用
如果你的應用是一直運行的,比如 web 應用,那麼可以使用 net/http/pprof
庫,它能夠對 Http 服務進行分析。
在 import 裏添加一行:
import _ "net/http/pprof"
在主函數中啓動服務監聽端口:
go func() {
http.ListenAndServe(":6060", nil)
}()
訪問 /debug/pprof
即可得到下面的內容:
/debug/pprof/
profiles:
0 block
756 goroutine
16100 heap
0 mutex
94 threadcreate
full goroutine stack dump
go tool ppof 獲取和分析 profile 數據
有了 profile 數據之後(不管是文件還是網絡請求),下一步就是要對這些數據進行分析。我們可以使用 go tool pprof
命令行工具。
在後面我們會生成調用關係圖和火焰圖,需要安裝 graphviz
軟件包,在 ubuntu 系統可以使用下面的命令:
$ sudo apt-get install -y graphviz
注意獲取的 profile 數據是動態的,要想獲得有效的數據,請保證應用處於較大的負載(比如正在運行的服務,或者通過其他工具模擬訪問壓力)。否則如果應用處於空閒狀態,得到的結果可能沒有任何意義。
我們以 CPU profile 分析爲例介紹兩種分析方法。
交互式終端
go tool pprof
最簡單的使用方式爲 go tool pprof [binary] [source]
,binary
是應用的二進制文件,用來解析各種符號;source
表示 profile 數據的來源,可以是本地的文件,也可以是 http 地址。比如:
➜ go tool pprof ./hyperkube http://172.16.3.232:10251/debug/pprof/profile
Fetching profile from http://172.16.3.232:10251/debug/pprof/profile
Please wait... (30s)
Saved profile in /home/cizixs/pprof/pprof.hyperkube.172.16.3.232:10251.samples.cpu.002.pb.gz
Entering interactive mode (type "help" for commands)
(pprof)
這個命令會進行 CPU profile 分析,等待一段時間(默認是 30s,如果在 url 最後加上 ?seconds=60
參數可以調整採集數據的時間爲 60s)之後,我們就進入了一個交互式命令行,可以對解析的結果進行查看和導出。可以通過 help
來查看支持的命令有哪些。
一個有用的命令是 topN
,它列出最耗時間的地方:
(pprof) top10
130ms of 360ms total (36.11%)
Showing top 10 nodes out of 180 (cum >= 10ms)
flat flat% sum% cum cum%
20ms 5.56% 5.56% 100ms 27.78% encoding/json.(*decodeState).object
20ms 5.56% 11.11% 20ms 5.56% runtime.(*mspan).refillAllocCache
20ms 5.56% 16.67% 20ms 5.56% runtime.futex
10ms 2.78% 19.44% 10ms 2.78% encoding/json.(*decodeState).literalStore
10ms 2.78% 22.22% 10ms 2.78% encoding/json.(*decodeState).scanWhile
10ms 2.78% 25.00% 40ms 11.11% encoding/json.checkValid
10ms 2.78% 27.78% 10ms 2.78% encoding/json.simpleLetterEqualFold
10ms 2.78% 30.56% 10ms 2.78% encoding/json.stateBeginValue
10ms 2.78% 33.33% 10ms 2.78% encoding/json.stateEndValue
10ms 2.78% 36.11% 10ms 2.78% encoding/json.stateInString
每一行表示一個函數的信息。前兩列表示函數在 CPU 上運行的時間以及百分比;第三列是當前所有函數累加使用 CPU 的比例;第四列和第五列代表這個函數以及子函數運行所佔用的時間和比例(也被稱爲累加值 cumulative
),應該大於等於前兩列的值;最後一列就是函數的名字。如果應用程序有性能問題,上面這些信息應該能告訴我們時間都花費在哪些函數的執行上了。
pprof 不僅能打印出最耗時的地方(top
),還能列出函數代碼以及對應的取樣數據、彙編代碼以及對應的取樣數據。list
命令後面跟着一個正則表達式,就能查看匹配函數的代碼以及每行代碼的耗時:
(pprof) list podFitsOnNode
Total: 120ms
ROUTINE ======================== k8s.io/kubernetes/plugin/pkg/scheduler.podFitsOnNode in /home/cizixs/go/src/k8s.io/kubernetes/_output/local/go/src/k8s.io/kubernetes/plugin/pkg/scheduler/generic_scheduler.go
0 20ms (flat, cum) 16.67% of Total
. . 230:
. . 231:// Checks whether node with a given name and NodeInfo satisfies all predicateFuncs.
. . 232:func podFitsOnNode(pod *api.Pod, meta interface{}, info *schedulercache.NodeInfo, predicateFuncs map[string]algorithm.FitPredicate) (bool, []algorithm.PredicateFailureReason, error) {
. . 233: var failedPredicates []algorithm.PredicateFailureReason
. . 234: for _, predicate := range predicateFuncs {
. 20ms 235: fit, reasons, err := predicate(pod, meta, info)
. . 236: if err != nil {
. . 237: err := fmt.Errorf("SchedulerPredicates failed due to %v, which is unexpected.", err)
. . 238: return false, []algorithm.PredicateFailureReason{}, err
. . 239: }
. . 240: if !fit {
如果想要了解對應的彙編代碼,可以使用 disadm <regex>
命令。
可視化
pprof 能以各種樣式輸出數據,比如 svg、gv、callgrind、png、gif 等等。其中一個非常便利的方法是在交互式終端中輸入 web 命令
,就能自動生成一個 svg
文件,並跳轉到瀏覽器打開,生成了一個函數調用圖:
這個調用圖包含了更多的信息,而且可視化的圖像能讓我們更清楚地理解整個應用程序的全貌。圖中每個方框對應一個函數,方框越大代表執行的時間越久(包括它調用的子函數執行時間,但並不是正比的關係);方框之間的箭頭代表着調用關係,箭頭上的數字代表被調用函數的執行時間。
因爲原圖比較大,這裏只截取了其中一部分,但是能明顯看到 encoding/json.(*decodeState).object
是這裏耗時比較多的地方,而且能看到它調用了哪些函數。這些信息對於定位和調優性能是非常有幫助的。如果想進一步在瀏覽器中查看源代碼和彙編代碼,可以使用 weblist 命令,和 list、disadm 的用法相同,它
能夠同時顯示源代碼和彙編代碼。
此外還可以輸入 pdf 命令
生成一個 pdf
文件。更詳細的 pprof 使用方法可以參考 pprof --help
或者 pprof 文檔。
另一個可視化的方法是直接啓動一個 http 服務:
go tool pprof -http="10.224.27.152:8081" ./hyperkube http://172.16.3.232:10251/debug/pprof/profile
在瀏覽器上訪問 10.224.27.152:8081 即可看到各種界面。
注:本文大量使用了 使用 pprof 和火焰圖調試 golang 應用 的內容,並結合了筆者平時的實踐。