Go語言核心36講(Go語言實戰與應用二十六)--學習筆記

MingsonZheng發表於2021-12-19

48 | 程式效能分析基礎(上)

作為拾遺的部分,今天我們來講講與 Go 程式效能分析有關的基礎知識。

Go 語言為程式開發者們提供了豐富的效能分析 API,和非常好用的標準工具。這些 API 主要存在於:

1、runtime/pprof;

2、net/http/pprof;

3、runtime/trace;

這三個程式碼包中。

另外,runtime程式碼包中還包含了一些更底層的 API。它們可以被用來收集或輸出 Go 程式執行過程中的一些關鍵指標,並幫助我們生成相應的概要檔案以供後續分析時使用。

至於標準工具,主要有go tool pprof和go tool trace這兩個。它們可以解析概要檔案中的資訊,並以人類易讀的方式把這些資訊展示出來。

此外,go test命令也可以在程式測試完成後生成概要檔案。如此一來,我們就可以很方便地使用前面那兩個工具讀取概要檔案,並對被測程式的效能加以分析。這無疑會讓程式效能測試的一手資料更加豐富,結果更加精確和可信。

在 Go 語言中,用於分析程式效能的概要檔案有三種,分別是:CPU 概要檔案(CPU Profile)、記憶體概要檔案(Mem Profile)和阻塞概要檔案(Block Profile)。

這些概要檔案中包含的都是:在某一段時間內,對 Go 程式的相關指標進行多次取樣後得到的概要資訊。

對於 CPU 概要檔案來說,其中的每一段獨立的概要資訊都記錄著,在進行某一次取樣的那個時刻,CPU 上正在執行的 Go 程式碼。

而對於記憶體概要檔案,其中的每一段概要資訊都記載著,在某個取樣時刻,正在執行的 Go 程式碼以及堆記憶體的使用情況,這裡包含已分配和已釋放的位元組數量和物件數量。至於阻塞概要檔案,其中的每一段概要資訊,都代表著 Go 程式中的一個 goroutine 阻塞事件。

注意,在預設情況下,這些概要檔案中的資訊並不是普通的文字,它們都是以二進位制的形式展現的。如果你使用一個常規的文字編輯器檢視它們的話,那麼肯定會看到一堆“亂碼”。

這時就可以顯現出go tool pprof這個工具的作用了。我們可以通過它進入一個基於命令列的互動式介面,並對指定的概要檔案進行查閱。就像下面這樣:

$ go tool pprof cpuprofile.out
Type: cpu
Time: Nov 9, 2018 at 4:31pm (CST)
Duration: 7.96s, Total samples = 6.88s (86.38%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) 

關於這個工具的具體用法,我就不在這裡贅述了。在進入這個工具的互動式介面之後,我們只要輸入指令help並按下Enter鍵,就可以看到很詳細的幫助文件。

我們現在來說說怎樣生成概要檔案。

你可能會問,既然在概要檔案中的資訊不是普通的文字,那麼它們到底是什麼格式的呢?一個對廣大的程式開發者而言,並不那麼重要的事實是,它們是通過 protocol buffers 生成的二進位制資料流,或者說位元組流。

概括來講,protocol buffers 是一種資料序列化協議,同時也是一個序列化工具。它可以把一個值,比如一個結構體或者一個字典,轉換成一段位元組流。

也可以反過來,把經過它生成的位元組流反向轉換為程式中的一個值。前者就被叫做序列化,而後者則被稱為反序列化。

換句話說,protocol buffers 定義和實現了一種“可以讓資料在結構形態和扁平形態之間互相轉換”的方式。

Protocol buffers 的優勢有不少。比如,它可以在序列化資料的同時對資料進行壓縮,所以它生成的位元組流,通常都要比相同資料的其他格式(例如 XML 和 JSON)佔用的空間明顯小很多。

又比如,它既能讓我們自己去定義資料序列化和結構化的格式,也允許我們在保證向後相容的前提下去更新這種格式。

正因為這些優勢,Go 語言從 1.8 版本開始,把所有 profile 相關的資訊生成工作都交給 protocol buffers 來做了。這也是我們在上述概要檔案中,看不到普通文字的根本原因了。

Protocol buffers 的用途非常廣泛,並且在諸如資料儲存、資料傳輸等任務中有著很高的使用率。不過,關於它,我暫時就介紹到這裡。你目前知道這些也就足夠了。你並不用關心runtime/pprof包以及runtime包中的程式是如何序列化這些概要資訊的。

繼續回到怎樣生成概要檔案的話題,我們依然通過具體的問題來講述。

我們今天的問題是:怎樣讓程式對 CPU 概要資訊進行取樣?

這道題的典型回答是這樣的。

這需要用到runtime/pprof包中的 API。更具體地說,在我們想讓程式開始對 CPU 概要資訊進行取樣的時候,需要呼叫這個程式碼包中的StartCPUProfile函式,而在停止取樣的時候則需要呼叫該包中的StopCPUProfile函式。

問題解析

runtime/pprof.StartCPUProfile函式(以下簡稱StartCPUProfile函式)在被呼叫的時候,先會去設定 CPU 概要資訊的取樣頻率,並會在單獨的 goroutine 中進行 CPU 概要資訊的收集和輸出。

注意,StartCPUProfile函式設定的取樣頻率總是固定的,即:100赫茲。也就是說,每秒取樣100次,或者說每10毫秒取樣一次。

赫茲,也稱 Hz,是從英文單詞“Hertz”(一個英文姓氏)音譯過來的一箇中文詞。它是 CPU 主頻的基本單位。

CPU 的主頻指的是,CPU 核心工作的時脈頻率,也常被稱為 CPU clock speed。這個時脈頻率的倒數即為時鐘週期(clock cycle),也就是一個 CPU 核心執行一條運算指令所需的時間,單位是秒。

例如,主頻為1000Hz 的 CPU,它的單個核心執行一條運算指令所需的時間為0.001秒,即1毫秒。又例如,我們現在常用的3.2GHz 的多核 CPU,其單個核心在1個納秒的時間裡就可以至少執行三條運算指令。

StartCPUProfile函式設定的 CPU 概要資訊取樣頻率,相對於現代的 CPU 主頻來說是非常低的。這主要有兩個方面的原因。

一方面,過高的取樣頻率會對 Go 程式的執行效率造成很明顯的負面影響。因此,runtime包中SetCPUProfileRate函式在被呼叫的時候,會保證取樣頻率不超過1MHz(兆赫),也就是說,它只允許每1微秒最多采樣一次。StartCPUProfile函式正是通過呼叫這個函式來設定 CPU 概要資訊的取樣頻率的。

另一方面,經過大量的實驗,Go 語言團隊發現100Hz 是一個比較合適的設定。因為這樣做既可以得到足夠多、足夠有用的概要資訊,又不至於讓程式的執行出現停滯。另外,作業系統對高頻取樣的處理能力也是有限的,一般情況下,超過500Hz 就很可能得不到及時的響應了。

在StartCPUProfile函式執行之後,一個新啟用的 goroutine 將會負責執行 CPU 概要資訊的收集和輸出,直到runtime/pprof包中的StopCPUProfile函式被成功呼叫。

StopCPUProfile函式也會呼叫runtime.SetCPUProfileRate函式,並把引數值(也就是取樣頻率)設為0。這會讓針對 CPU 概要資訊的取樣工作停止。

同時,它也會給負責收集 CPU 概要資訊的程式碼一個“訊號”,以告知收集工作也需要停止了。

在接到這樣的“訊號”之後,那部分程式將會把這段時間內收集到的所有 CPU 概要資訊,全部寫入到我們在呼叫StartCPUProfile函式的時候指定的寫入器中。只有在上述操作全部完成之後,StopCPUProfile函式才會返回。

好了,經過這一番解釋,你應該已經對 CPU 概要資訊的取樣工作有一定的認識了。你可以去看看 demo96.go 檔案中的程式碼,並執行幾次試試。這樣會有助於你加深對這個問題的理解。

package main

import (
	"errors"
	"fmt"
	"os"
	"puzzlers/article37/common"
	"puzzlers/article37/common/op"
	"runtime/pprof"
)

var (
	profileName = "cpuprofile.out"
)

func main() {
	f, err := common.CreateFile("", profileName)
	if err != nil {
		fmt.Printf("CPU profile creation error: %v\n", err)
		return
	}
	defer f.Close()
	if err := startCPUProfile(f); err != nil {
		fmt.Printf("CPU profile start error: %v\n", err)
		return
	}
	if err = common.Execute(op.CPUProfile, 10); err != nil {
		fmt.Printf("execute error: %v\n", err)
		return
	}
	stopCPUProfile()
}

func startCPUProfile(f *os.File) error {
	if f == nil {
		return errors.New("nil file")
	}
	return pprof.StartCPUProfile(f)
}

func stopCPUProfile() {
	pprof.StopCPUProfile()
}

總結

我們這兩篇內容講的是 Go 程式的效能分析,這其中的內容都是你從事這項任務必備的一些知識和技巧。

首先,我們需要知道,與程式效能分析有關的 API 主要存在於runtime、runtime/pprof和net/http/pprof這幾個程式碼包中。它們可以幫助我們收集相應的效能概要資訊,並把這些資訊輸出到我們指定的地方。

Go 語言的執行時系統會根據要求對程式的相關指標進行多次取樣,並對取樣的結果進行組織和整理,最後形成一份完整的效能分析報告。這份報告就是我們一直在說的概要資訊的彙總。

一般情況下,我們會把概要資訊輸出到檔案。根據概要資訊的不同,概要檔案的種類主要有三個,分別是:CPU 概要檔案(CPU Profile)、記憶體概要檔案(Mem Profile)和阻塞概要檔案(Block Profile)。

筆記原始碼

https://github.com/MingsonZheng/go-core-demo

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章