golang 性能調優分析工具 pprof (上)

一、golang 程序性能調優

在 golang 程序中,有哪些內容需要調試優化?

一般常規內容:

  1. cpu:程序對cpu的使用情況 – 使用時長,佔比等
  2. 內存:程序對cpu的使用情況 – 使用時長,佔比,內存泄露等。如果在往裡分,程序堆、棧使用情況
  3. I/O:IO的使用情況 – 哪個程序IO佔用時間比較長

golang 程序中:

  1. goroutine:go的協程使用情況,調用鏈的情況
  2. goroutine leak:goroutine泄露檢查
  3. go dead lock:死鎖的檢測分析
  4. data race detector:數據競爭分析,其實也與死鎖分析有關

上面是在 golang 程序中,性能調優的一些內容。

有什麼方法工具調試優化 golang 程序?

比如 linux 中 cpu 性能調試,工具有 top,dstat,perf 等。

那麼在 golang 中,有哪些分析方法?

golang 性能調試優化方法:

  • Benchmark基準測試,對特定代碼的運行時間和內存信息等進行測試
  • Profiling程序分析,程序的運行畫像,在程序執行期間,通過採樣收集的數據對程序進行分析
  • Trace跟蹤,在程序執行期間,通過採集發生的事件數據對程序進行分析

profiling 和 trace 有啥區別?
profiling 分析沒有時間線,trace 分析有時間線。

在 golang 中,應用方法的工具呢?

這裡介紹 pprof 這個 golang 工具,它可以幫助我們調試優化程序。

它的最原始程序是 gperftools//github.com/gperftools/gperftools,golang 的 pprof 是從它而來的。

二、pprof 介紹

簡介

pprof 是 golang 官方提供的性能調優分析工具,可以對程序進行性能分析,並可視化數據,看起來相當的直觀。
當你的 go 程序遇到性能瓶頸時,可以使用這個工具來進行調試並優化程序。

本文將對下面 golang 中 2 個監控性能的包 pprof 進行運用:

  • runtime/pprof:採集程序運行數據進行性能分析,一般用於後台工具型應用。
  • net/http/pprof:對 runtime/pprof 的二次封裝,一般應用於 http server ,採集 http server 運行時數據進行分析。

pprof 開啟後,每隔一段時間就會採集當前程序的堆棧信息,獲取函數的 cpu、內存等使用情況。通過對採樣的數據進行分析,形成一個數據分析報告。

pprof 以 profile.proto 的格式保存數據,然後根據這個數據生成可視化的分析報告,支持文本形式和圖形形式報告。
profile.proto 里具體的數據格式是 protocol buffers

pprof 使用模式

  • Report generation:報告生成

  • Interactive terminal use:交互式終端

  • Web interface:Web 界面

三、runtime/pprof

前提條件

先安裝 pprof: go get -u github.com/google/pprof

調試分析 golang 程序,要開啟 profile 然後開始採樣數據。

採樣數據的方式:

  • 第 1 種,在 go 程序中添加如下代碼:
    StartCPUProfile 為當前 process 開啟 CPU profiling 。
    StopCPUProfile 停止當前的 CPU profile。當所有的 profile 寫完了後它才返回。
// 開啟 cpu 採集分析:
pprof.StartCPUProfile(w io.Writer)

// 停止 cpu 採集分析:
pprof.StopCPUProfile()

WriteHeapProfile 把內存 heap 相關的內容寫入到文件中

pprof.WriteHeapProfile(w io.Writer)
  • 第 2 種,在 benchmark 測試的時候
go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
  • 還有一種,對 http server 採集數據
go tool pprof $host/debug/pprof/profile

程序示例

go version go1.13.9

例子 1

我們用第 1 種方法,在程序中添加分析代碼,demo.go :

package main

import (
	"bytes"
	"flag"
	"log"
	"math/rand"
	"os"
	"runtime"
	"runtime/pprof"
	"sync"
)

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write mem profile to `file`")

func main() {
	flag.Parse()
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal("could not create CPU profile: ", err)
		}
		defer f.Close()

		if err := pprof.StartCPUProfile(f); err != nil {
			log.Fatal("could not start CPU profile: ", err)
		}
		defer pprof.StopCPUProfile()
	}

	var wg sync.WaitGroup
	wg.Add(200)

	for i := 0; i < 200; i++ {
		go cyclenum(30000, &wg)
	}

	writeBytes()

	wg.Wait()

	if *memprofile != "" {
		f, err := os.Create(*memprofile)
		if err != nil {
			log.Fatal("could not create memory profile: ", err)
		}
		defer f.Close()
		runtime.GC()

		if err := pprof.WriteHeapProfile(f); err != nil {
			log.Fatal("cound not write memory profile: ", err)
		}
	}
}

func cyclenum(num int, wg *sync.WaitGroup) {
	slice := make([]int, 0)
	for i := 0; i < num; i++ {
		for j := 0; j < num; j++ {
			j = i + j
			slice = append(slice, j)
		}
	}
	wg.Done()
}

func writeBytes() *bytes.Buffer {
	var buff bytes.Buffer

	for i := 0; i < 30000; i++ {
		buff.Write([]byte{'0' + byte(rand.Intn(10))})
	}
	return &buff
}

編譯程序、採集數據、分析程序:

  1. 編譯 demo.go
go build demo.go
  1. 用 pprof 採集數據,命令如下:
./demo.exe --cpuprofile=democpu.pprof  --memprofile=demomem.pprof

說明:我是 win 系統,這個 demo 就是 demo.exe ,linux 下是 demo

  1. 分析數據,命令如下:
go tool pprof democpu.pprof

go tool pprof 簡單的使用格式為:go tool pprof [binary] [source]

  • binary: 是應用的二進制文件,用來解析各種符號
  • source: 表示 profile 數據的來源,可以是本地的文件,也可以是 http 地址

要了解 go tool pprof 更多命令使用方法,請查看文檔:go tool pprof --help

注意

獲取的 Profiling 數據是動態的,要想獲得有效的數據,請保證應用處於較大的負載(比如正在生成中運行的服務,或者通過其他工具模擬訪問壓力)。否則如果應用處於空閑狀態,得到的結果可能沒有任何意義。
(後面會遇到這種問題)

分析數據,基本的模式有 2 種:

  • 一個是命令行交互分析模式
  • 一個是圖形可視化分析模式

命令行交互分析

  1. 分析上面採集的數據,命令: go tool pprof democpu.pprof

字段 說明
Type 分析類型,這裡是 cpu
Duration 程序執行的時長

Duration 下面還有一行提示,這是交互模式(通過輸入 help 獲取幫助信息,輸入 o 獲取選項信息)。

可以看出,go 的 pprof 操作還有很多其他命令。

  1. 輸入 help 命令,出來很多幫助信息:

Commands 下有很多命令信息,text ,top 2個命令解釋相同,輸入這個 2 個看看:

  1. 輸入 top,text 命令

top 命令:對函數的 cpu 耗時和百分比排序後輸出

top後面還可以帶參數,比如: top 15

輸出了相同的信息。

字段 說明
flat 當前函數佔用 cpu 耗時
flat % 當前函數佔用 cpu 耗時百分比
sum% 函數佔用 cpu 時間累積佔比,從小到大一直累積到 100%
cum 當前函數加上調用當前函數的函數佔用 cpu 的總耗時
%cum 當前函數加上調用當前函數的函數佔用 cpu 的總耗時佔比

從字段數據我們可以看出哪一個函數比較耗費時間,就可以對這個函數進一步分析。
分析用到的命令是 list

list 命令:可以列出函數最耗時的代碼部分,格式:list 函數名

從上面採樣數據可以分析出總耗時最長的函數是 main.cycylenum,用 list cyclenum 命令進行分析,如下圖:

發現最耗時的代碼是 62 行:slice = append(slice, j) ,這裡耗時有 1.47s ,可以對這個地方進行優化。

這裡耗時的原因,應該是 slice 的實時擴容引起的。那我們空間換時間,固定 slice 的容量,make([]int, num * num)

可視化分析

A. pprof 圖形可視化

除了上面的命令行交互分析,還可以用圖形化來分析程序性能。

圖形化分析前,先要安裝 graphviz 軟件,

下載對應的平台安裝包,安裝完成後,把執行文件 bin 放入 Path 環境變量中,然後在終端輸入 dot -version 命令查看是否安裝成功。

生成可視化文件:

有 2 個步驟,根據上面採集的數據文件 democpu.pprof 來進行可視化:

  1. 命令行輸入:go tool pprof democpu.pprof
  2. 輸入 web 命令

在命令行里輸入 web 命令,就可以生成一個 svg 格式的文件,用瀏覽器打開即可查看 svg 文件。

執行上面 2 個命令如下圖:

用瀏覽器查看生成的 svg 圖:

(文件太大,只截取了一小部分圖,完整的圖請自行生成查看)

關於圖形的一點說明:

  1. 每個框代表一個函數,理論上框越大表示佔用的 cpu 資源越多
  2. 每個框之間的線條代表函數之間的調用關係,線條上的數字表示函數調用的次數
  3. 每個框中第一行數字表示當前函數佔用 cpu 的百分比,第二行數字表示當前函數累計佔用 cpu 的百分比
B. 火焰圖 Flame Graph

火焰圖 (Flame Graph) 是性能優化專家 Bredan Gregg 創建的一種性能分析圖。Flame Graphs visualize profiled code。

火焰圖形狀如下:

(來自://github.com/brendangregg/FlameGraph)

上面用 pprof 生成的採樣數據,要把它轉換成火焰圖,就要使用一個轉換工具 go-torch,這個工具是 uber 開源,它是用 go 語言編寫的,可以直接讀取 pprof 採集的數據,並生成一張火焰圖, svg 格式的文件。

  1. 安裝 go-torch:

go get -v github.com/uber/go-torch

  1. 安裝 flame graph:

git clone //github.com/brendangregg/FlameGraph.git

  1. 安裝 perl 環境:

生成火焰圖的程序 FlameGraph 是用 perl 寫的,所以先要安裝執行 perl 語言的環境。

  • 安裝 perl 環境://www.perl.org/get.html
  • 把執行文件 bin 加入 Path 中
  • 在終端下執行命令:perl -h ,輸出了幫助信息,則說明安裝成功

  1. 驗證 FlameGraph 是否安裝成功:

進入到 FlameGraph 安裝目錄,執行命令,./flamegraph.pl --help

輸出信息說明安裝成功

  1. 生成火焰圖:

重新進入到文件 democpu.pprof 的目錄,然後執行命令:

go-torch -b democpu.pprof

上面命令默認生成名為 torch.svg 的文件,用瀏覽器打開查看:

自定義輸出文件名,後面加 -f 參數:

go-torch -b democpu.pprof -f cpu_flamegraph.svg

火焰圖說明:

火焰圖 svg 文件,你可以點擊上面的每個方塊來查看分析它上面的內容。

火焰圖的調用順序從下到上,每個方塊代表一個函數,它上面一層表示這個函數會調用哪些函數,方塊的大小代表了佔用 CPU 使用時長長短。

go-torch 的命令格式:

go-torch [options] [binary] <profile source>

go-torch 幫助文檔:

想了解更多 go-torch 用法,請用 help 命令查看幫助文檔,go-torch --help

或查看 go-torch README 文檔 。

四、參考