天天看點

火焰圖對 Go 程式進行性能分析

軟體工程中,系統上線之後,仍需要持續對系統進行優化或者重構。

學會對應用系統進行運作時資料采集與性能分析是軟體工程實踐常用的基本技能。通常使用

profile

表示性能分析與采集,或者使用 profiling 代表性能分析這個行為。比如 Java 語言中相關的工具為

jprofiler

,意為 Java Profiler。

Go 語言非常注重性能,其内置庫裡就自帶了性能分析庫 pprof。pprof 有兩個包用來分析程式: runtime/pprof 與 net/http/pprof,其中 net/http/pprof 隻是對 runtime/pprof 包進行封裝并用 http 暴露出來。runtime/pprof 用于對普通的應用程式進行性能分析,主要用于可結束的代碼塊,比如一次函數調用;而 net/http/pprof 專門用于對背景服務型程式的性能采集與分析。

本小節将會介紹如何基于 pprof 進行性能分析與優化,包括 CPU 、記憶體占用、Block 阻塞以及 Goroutine 使用等方面。除此之外,還會介紹更加直覺的圖形工具:火焰圖,基于 go-torch 将 pprof 的結果轉換成火焰圖。

普通應用程式的性能分析

我們已經知道,runtime/pprof 用于對普通的應用程式進行性能分析,主要用于可結束的代碼塊。是以,我們下面通過案例來實踐。

計算圓周率

筆者選取的案例是計算圓周率的算法。

衆所周知,可以說,它是世界上最有名的無理常數了,代表的是一個圓的周長與直徑之比或稱為“圓周率”。公元前 250 年左右,阿基米德給出了“圓周率”的估計值在 223/71~22/7 之間。

中國南北朝時期的著名數學家祖沖之(429-500)首次将“圓周率”精算到小數第七位,即在 3.1415926 至 3.1415927 之間,他提出的“密率與約率”對數學的研究有重大貢獻。直到 15 世紀,阿拉伯數學家阿爾·卡西才以“精确到小數點後17位”打破了這一紀錄。

代表“圓周率”的字母是第十六個希臘字母的小寫。也是希臘語 περιφρεια(表示周邊,地域,圓周)的首字母。1706 年英國數學家威廉·瓊斯(William Jones, 1675-1749)最先使用“π”來表示圓周率。1736 年,瑞士數學家歐拉(Leonhard Euler, 1707-1783)也開始用表示圓周率。從此,便成了圓周率的代名詞。

通常的計算方法有如下幾種:

  • 蒙特卡羅法;
  • 正方形逼近;
  • 疊代法;
  • 丘德諾夫斯基公式

測試代碼的實作

筆者這裡采用蒙特卡羅方法計算圓周率,大緻思路如下:

正方形内部有一個相切的圓,它們的面積之比是π/4。 在這個正方形内部,随機産生10000個點(即10000個坐标對 (x, y)),計算它們與中心點的距離,進而判斷是否落在圓的内部。 如果這些點均勻分布,那麼圓内的點應該占到所有點的 π/4,是以将這個比值乘以4,就是π的值。通過随機模拟30000個點,π的估算值與真實值相差0.07%。

最後,實作的完整代碼如下所示:

package main import ( "flag" "fmt" "log" "os" "runtime" "runtime/pprof" "time" ) var n int64 = 10000000000 var h float64 = 1.0 / float64(n) func f(a float64) float64 { return 4.0 / (1.0 + a*a) } func chunk(start, end int64, c chan float64) { var sum float64 = 0.0 for i := start; i < end; i++ {  x := h * (float64(i) + 0.5)  sum += f(x) } c <- sum * h } func main() { var cpuProfile = flag.String("cpuprofile", "", "write cpu profile to file") var memProfile = flag.String("memprofile", "", "write mem profile to file") flag.Parse() //采樣cpu運作狀态 if *cpuProfile != "" {  f, err := os.Create(*cpuProfile) if err != nil {   log.Fatal(err)  }  pprof.StartCPUProfile(f)  defer pprof.StopCPUProfile() } //記錄開始時間 start := time.Now() var pi float64 np := runtime.NumCPU() runtime.GOMAXPROCS(np) c := make(chan float64, np) fmt.Println("np: ", np) for i := 0; i < np; i++ {    //利用多處理器,并發處理  go chunk(int64(i)*n/int64(np), (int64(i)+1)*n/int64(np), c) } for i := 0; i < np; i++ {  tmp := <-c  fmt.Println("c->: ", tmp)  pi += tmp  fmt.Println("pai: ", pi) } fmt.Println("Pi: ", pi) //記錄結束時間 end := time.Now() //輸出執行時間,機關為毫秒。 fmt.Printf("spend time: %vs\n", end.Sub(start).Seconds()) //采樣 memory 狀态 if *memProfile != "" {  f, err := os.Create(*memProfile) if err != nil {   log.Fatal(err)  }  pprof.WriteHeapProfile(f)  f.Close() } }

如上就是計算 π 的算法,基于 go 語言的 goroutine和 channel,充分利用多核處理器,提高 CPU 資源計算的速度。

我們在依賴中引入了 runtime/pprof,在實作的代碼中添加了相關的 CPU Profiling 和 Memory Profiling 代碼就可以實作 CPU 和記憶體的性能評測。

編譯與執行

接着就是編譯獲得可執行檔案,執行後獲得 pprof 的采樣資料,然後就可以利用相關工具進行分析。相關的指令如下:

$ go build  -o pai main.go $ ./pai --cpuprofile=cpu.pprof $ ./pai --memprofile=mem.pprof

火焰圖對 Go 程式進行性能分析

上面的指令依次生成了 cpu.pprof 和 mem.pprof 兩個采樣檔案,我們使用

go tool pprof

指令進行分析:

$ go tool pprof cpu.pprof

執行完上述指令即進入 pprof 指令行互動模式。pprof 支援多個指令,比如 top 用于顯示 pprof 檔案中的前 10 項資料,可以通過top 20等方式顯示20行資料;其他的指令如 list,pdf、eog 等。

上圖中,其他的一些參數解釋如下:

  • Duration:程式執行時間。多核執行程式,總計耗時 13.47s,而采樣時間為 24.44s;每個核均分采樣時間。
  • flat/flat%:分别表示在目前層級 CPU 的占用時間和百分比。
  • cum/cum%:分别表示截止到目前層級累積的 CPU 時間和占比。
  • sum%:所有層級的 CPU 時間累積占用,從小到大一直累積到100%,即 24.44s。

本例中,main.chunk 在目前層級占用 CPU 時間 21.86s,占比本次采集時間的 89.44%。而該函數累積占用時間 24.44s,占本次采集時間的 100%。通過 cum 資料可以看到,chunk 函數的 CPU 占用時間最多。

上圖很清楚的說明了應用程式耗時的主要函數,接着就利用 list 指令檢視占用的主要因素。list 指令根據你的正規表達式輸出相關的方法,直接跟可選項 -o 會輸出所有的方法,也可以指定方法名。這樣就能檢視比對函數的代碼以及每行代碼的耗時:

火焰圖對 Go 程式進行性能分析

從上圖可以看出,在第 24 行調用函數 f(x) 還額外花了 2.58s,每一行代碼花費的時間都有顯示出來,根據這些資訊可以開展代碼的優化。

圖形化渲染

對于 pprof 采集的結果,我們不僅可以使用 pprof 自帶的指令進行分析,還可以通過更加直覺的矢量圖進行分析。借助于 graphviz,pprof 可以直接生成對應的圖像化檔案。

筆試基于 Centos 7.5 系統,通過如下的指令直接安裝 graphviz:

$ sudo yum install graphviz

更多系統環境的安裝說明,請參見

graphviz 官網

安裝好 graphviz,繼續在 pprof 互動指令行中輸入 svg:

火焰圖對 Go 程式進行性能分析

注意 web 指令在伺服器類型的系統不支援,通過 svg 指令來生成矢量圖,使用浏覽器打開,如下所示:

火焰圖對 Go 程式進行性能分析

筆者截取了部分内容,從上圖同樣可以看到,主要耗時的函數為 main.chunk,耗時時間為 21.86s,關聯調用的函數 f(x) 耗時為 2.58s。圖中各個方塊的大小也代表 CPU 占用的情況,方塊越大說明占用 CPU 時間越長。

背景服務程式的性能分析

針對一直運作的背景服務,比如 web 應用或者分布式應用,我們可以使用 net/http/pprof 庫,它能夠在應用提供 HTTP 服務時進行分析。

pprof 采集背景服務,如果使用了預設的 http.DefaultServeMux,通常是代碼直接使用 http.ListenAndServe("0.0.0.0:8000", nil),這種情況則比較簡單,隻需要導入包即可。

import ( _ "net/http/pprof" )

注意該包利用下劃線"_"導入,意味着我們隻需要該包運作其init()函數即可,如此該包将自動完成資訊采集并儲存在記憶體中。

如果你使用自定義的 ServerMux複用器,則需要手動注冊一些路由規則:

r.HandleFunc("/debug/pprof/", pprof.Index) r.HandleFunc("/debug/pprof/heap", pprof.Index) r.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) r.HandleFunc("/debug/pprof/profile", pprof.Profile) r.HandleFunc("/debug/pprof/symbol", pprof.Symbol) r.HandleFunc("/debug/pprof/trace", pprof.Trace)

這些路徑分别表示:

  • /debug/pprof/profile:通路這個連結會自動進行 CPU profiling,持續 30s,并生成一個檔案供下載下傳,可以通過帶參數?=seconds=60進行60秒的資料采集。
  • /debug/pprof/block:Goroutine阻塞事件的記錄。預設每發生一次阻塞事件時取樣一次。
  • /debug/pprof/goroutines:活躍Goroutine的資訊的記錄。僅在擷取時取樣一次。
  • /debug/pprof/heap: 堆記憶體配置設定情況的記錄。預設每配置設定512K位元組時取樣一次。
  • /debug/pprof/mutex: 檢視争用互斥鎖的持有者。
  • /debug/pprof/threadcreate: 系統線程建立情況的記錄。 僅在擷取時取樣一次。

改寫測試代碼

将計算圓周率的程式改寫成一個服務,對外提供一個接口,并引入 net/http/pprof 依賴,來采集 HTTP 服務的性能名額。

package main import ( "fmt" "net/http" _ "net/http/pprof" "runtime" ) var n int64 = 10000000000 var h = 1.0 / float64(n) func f(a float64) float64 { return 4.0 / (1.0 + a*a) } func chunk(start, end int64, c chan float64) { var sum float64 = 0.0 for i := start; i < end; i++ {  x := h * (float64(i) + 0.5)  sum += f(x) } c <- sum * h } func callFunc(w http.ResponseWriter, r *http.Request) { var pi float64 np := runtime.NumCPU() runtime.GOMAXPROCS(np) c := make(chan float64, np) fmt.Println("np: ", np) for i := 0; i < np; i++ { go chunk(int64(i)*n/int64(np), (int64(i)+1)*n/int64(np), c) } for i := 0; i < np; i++ {  tmp := <-c  fmt.Println("c->: ", tmp)  pi += tmp  fmt.Println("pai: ", pi) } fmt.Println("Pi: ", pi) } func main() { http.HandleFunc("/getAPi", callFunc) http.ListenAndServe(":8000", nil) }

我們在上述代碼的實作中,對外暴露了 8000 端口,并定義了一個接口

getAPi

。計算圓周率的實作和之前相同,每次調用接口都将會觸發計算 π 一次。

編譯執行

該寫完代碼,我們就可以進行編譯和執行 HTTP 服務了,執行如下的指令:

$ go build -o httpapi main.go $ ./httpapi

将程式編譯成功之後,運作二進制檔案,可以擷取服務的性能資料後,

此時,我們就可以通過 pprof 的 HTTP 接口通路 http://localhost:8000/debug/pprof/:

火焰圖對 Go 程式進行性能分析

上圖展示了 pprof web 檢視服務的運作情況,同時不斷重新整理網頁可以發現采樣結果也在不斷更新。

圖形化分析

與上面可結束的程式進行性能分析一樣,我們對于背景程式也可以使用圖像化的方式分析性能。

接下來使用 go tool pprof 工具對這些資料進行分析和儲存了,一般都是使用 pprof 通過 HTTP 通路上面列的那些路由端點直接擷取到資料後再進行分析,擷取到資料後 pprof 會自動讓終端進入互動模式。

通過如下的指令檢視記憶體 Memory 相關情況:

$ go tool pprof main http://localhost:8000/debug/pprof/heap

火焰圖對 Go 程式進行性能分析

上述指令采集記憶體資訊,控制台輸出了生成的圖檔名稱:profile001.svg,預設在目前目錄,當然我們也可以指定位置和檔案名。

火焰圖對 Go 程式進行性能分析

由于沒有 http 請求的通路,是以記憶體的占用比較低,沒有任何異常。下面我們将通過壓測模拟線上情況,來分析在正常運作時的各項性能。

利用 go-torch 生成火焰圖

上面的小節介紹了 net/http/pprof 和 runtime/pprof 進行 Go 程式的性能分析。然而上面的案例僅僅隻是采樣了部分代碼段。同時隻有當有大量請求時才能看到應用服務的主要優化資訊。這時候就需要借助于另一款 Uber 開源的火焰圖工具 go-torch,以便輔助我們完成分析。要想實作火焰圖的效果,需要安裝如下 3 個工具:壓測元件 wrk、FlameGraph 火焰圖、go-torch 工具。下面将會依次介紹這三款元件的安裝使用。

壓測元件 wrk

wrk 是一款針對 HTTP 協定的基準測試工具,它能夠在單機多核 CPU 的條件下,使用系統自帶的高性能 I/O 機制,如 epoll,kqueue 等,通過多線程和事件模式,對目标機器産生大量的負載。安裝指令如下所示:

$ git clone https://github.com/brendangregg/FlameGraph.git $ cd wrk/ $ make

通過如上的指令,我們就生成了可執行的 wrk 檔案。其使用比較簡單,主要參數說明如下:

  • -c:總的連接配接數(每個線程處理的連接配接數=總連接配接數/線程數)
  • -d:測試的持續時間,如2s(2second),2m(2minute),2h(hour)
  • -t:需要執行的線程總數
  • -s:執行Lua腳本,這裡寫lua腳本的路徑和名稱,後面會給出案例
  • -H:需要添加的頭資訊,注意header的文法,舉例,-H “token: abcdef”,說明一下,token,冒号,空格,abcdefg(不要忘記空格,否則會報錯的)。

筆者剛開始執行的壓測參數如下:

./wrk -t5 -c10 -d120s http://localhost:8000/getAPi

即 5 個線程并發,每秒保持 10 個連接配接,持續時間 120s。如果出現如下的錯誤,

unable to create thread 419: Too many open files

這是由于 /socket連接配接數量超過系統設定值,則需要調整每個使用者最大允許打開檔案數量。

$ ulimit -n 2048

FlameGraph 火焰圖與 go-torch

火焰圖(flame graph)是性能分析的利器,通過它可以快速定位性能瓶頸點。在 Linux 伺服器,一般配合 perf 一起使用。

go-torch 是 uber 開源的一個工具,可以直接讀取 pprof的 profiling 資料,并生成一個火焰圖的 svg 檔案。火焰圖 svg 檔案可以通過浏覽器打開,它對于調用圖的優點是:可以通過點選每個方塊來分析它上面的内容。

執行如下的指令進行安裝:

$ git clone https://github.com/brendangregg/FlameGraph.git $ go get github.com/uber/go-torch

go-torch 使用的指令如下:

$ go-torch -u http://localhost:8000 -t 100

如上的指令将會開啟 go-torch 工具對 http://localhost:8000 采集 100s 資訊。

壓測生成火焰圖

安裝好上述三個元件之後,我們将會進行測試。首先是啟動我們的應用服務:

$ ./httpapi

接着啟動壓測和 go-torch:

$ ./wrk -t5 -c10 -d120s http://localhost:8000/getAPi $ go-torch -u http://localhost:8000 -t 100

火焰圖對 Go 程式進行性能分析
火焰圖對 Go 程式進行性能分析

可以看到,我們壓測的請求,已經在服務端生成相應的火焰圖:torch.svg。注:在 FlameGraph 目錄下執行 go-torch,否則需将該二進制可執行檔案的路徑添加到系統環境變量。

打開火焰圖,如下所示:

火焰圖對 Go 程式進行性能分析

火焰圖形似火焰,故此得名,其橫軸是 CPU 占用時間,縱軸是調用順序。火焰圖的調用順序從下到上,每個方塊代表一個函數,它上面一層表示這個函數會調用哪些函數,方塊的大小代表了占用 CPU 使用的長短。火焰圖的配色并沒有特殊的意義,預設的紅、黃配色是為了更像火焰而已。

與我們上面所分析的結果是一樣的,總體的耗時都在 chunk 函數。我們再來看一張沒有請求通路時的火焰圖:

火焰圖對 Go 程式進行性能分析

可以看到,這種情況 CPU 占用時間和記憶體占用非常平穩,主要集中在提供 http 服務的庫函數。

小結

本文主要介紹了如何通過 pprof 對 Go 應用程式進行性能名額的采集以及性能分析。我們通過 pprof 擷取到 CPU 和記憶體使用的細節,更進一步可以指導哪些函數耗時,函數之間的調用鍊。想更細緻分析,就要精确到代碼級别了,看看每行代碼的耗時,直接定位到出現性能問題的那行代碼。

結合 Uber 開源的 go-torch 生成火焰圖,從全局來檢視系統運作時的記憶體和 CPU,以及 Goroutines 和阻塞鎖等情況,熟練使用性能分析的工具,能夠幫助我們更快地定位線上問題并解決問題的 bug。

通過本文的講解,你也了解到,開啟背景程式的性能分析需要有請求,而不是靜态的服務,本文使用的是壓測來模拟大量的請求。當然在生産環境開啟 pprof 也是需要考慮性能的開銷,在上線前解決問題肯定是最好的選擇。