本篇講的是Go程序的性能分析,下面提到的內(nèi)容都是從事這項(xiàng)任務(wù)必備的一些知識(shí)和技巧。這些有助于我們真正理解以采樣、收集、輸出為代表的一系列操作步驟。
創(chuàng)新互聯(lián)公司2013年開創(chuàng)至今,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都網(wǎng)站設(shè)計(jì)、網(wǎng)站制作網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢想脫穎而出為使命,1280元高坪做網(wǎng)站,已為上家服務(wù),為高坪各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18980820575
Go語言為程序開發(fā)者們提供了豐富的性能分析API,和非常好用的標(biāo)準(zhǔn)工具。這些API主要存在于下面三個(gè)包中:
另外,runtime包中還包含了一些更底層的API。這些都可以被用來收集或輸出Go程序運(yùn)行過程中的一些關(guān)鍵指標(biāo),并幫助我們生成相應(yīng)的概要文件以供后續(xù)分析時(shí)使用。
標(biāo)準(zhǔn)工具主要有:
這兩個(gè)工具,可以解析概要文件中的信息,并以人類易讀的方式把這些信息展示出來。
go test命令,也可以在程序測試完成后生成概要文件。這樣就可以很方便的使用前面那兩個(gè)工具讀取概要文件,并對(duì)被測程序的性能加以分析。這樣就讓程序性能測試的資料更加豐富,結(jié)果也更加精確和可信。
在Go語言中,用于分析程序性能的概要文件有三種:
這些概要文件中包含的都是:在某一段時(shí)間內(nèi),對(duì)Go程序的相關(guān)指標(biāo)進(jìn)行多次采樣后得到的概要信息。
對(duì)于CPU概要文件,其中的每一段獨(dú)立的概要信息都記錄著在進(jìn)行某一次采樣的那個(gè)時(shí)刻,CPU上正在執(zhí)行的Go代碼。
對(duì)于內(nèi)存概要文件,其中的每一段概要信息都記載著在某個(gè)采樣時(shí)刻,正在執(zhí)行的Go代碼以及堆內(nèi)存的使用請(qǐng)求,這里包含已分配和已釋放的字節(jié)數(shù)量和對(duì)象數(shù)量。
對(duì)于阻塞概要文件,其中每一段概要信息都代表著Go程序中的一個(gè)goroutine的阻塞事件。
查看概要文件
在默認(rèn)情況下,這些概要文件中的信息并不是普通的文本,它們是以二進(jìn)制的形式展現(xiàn)的。如果使用常規(guī)的文本編輯器查看,看到的是亂碼。需要用go tool pprof這個(gè)工具來查看。可以通過該工具進(jìn)入一個(gè)基于命令行的交互式界面,并對(duì)指定的概要文件進(jìn)行查閱:
$ 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)
關(guān)于這個(gè)工具的具體用法沒有展開。建議在使用時(shí),輸入help查看幫助信息。
概要文件中的信息并不是普通的文本。而是通過protocol buffers生成的二進(jìn)制數(shù)據(jù)流,或者說字節(jié)流。而protocol buffers是一種數(shù)據(jù)序列化協(xié)議,同時(shí)也是一個(gè)序列化工具。它可以把一個(gè)值,比如一個(gè)結(jié)構(gòu)體或者一個(gè)字典,轉(zhuǎn)換成一段字節(jié)流。這個(gè)過程叫序列化。也可以反過來,把生成的字節(jié)流轉(zhuǎn)換為程序中的一個(gè)值,這叫反序列化。
Go語言從1.8版本開始,把所有的profile相關(guān)的信息生成工作都交給protocol buffers來做了。它有不少的優(yōu)勢。可以在序列化數(shù)據(jù)的同時(shí)對(duì)數(shù)據(jù)進(jìn)行壓縮,所以生成的字節(jié)流通常都要比其他格式(XML和JSON)占用的空間小很多。還支持自定義數(shù)據(jù)序列化和結(jié)構(gòu)化的格式,也允許在保證向后兼容的前提下更新這種格式。這就是概要文件不使用普通文本格式保存的原因。
順便提一下,protocol buffers的用途非常廣泛,并且在諸如數(shù)據(jù)存儲(chǔ)、數(shù)據(jù)傳輸?shù)热蝿?wù)中有著很高的使用率。
Protocol Buffers,是Google公司開發(fā)的一種數(shù)據(jù)描述語言,類似于XML能夠?qū)⒔Y(jié)構(gòu)化數(shù)據(jù)序列化,可用于數(shù)據(jù)存儲(chǔ)、通信協(xié)議等方面。
更多相關(guān)的知識(shí)就不展開的。
采樣CPU概要信息,需要用到runtime/pprof包中的API。要讓程序開始對(duì)CPU概要信息進(jìn)行采樣,需要調(diào)用包中的StartCPUProfile函數(shù)。而在停止采樣的時(shí)候,需要調(diào)用包中的StopCPUProfile函數(shù)。
runtime/pprof.StartCPUProfile函數(shù)在被調(diào)用的時(shí)候,先會(huì)去設(shè)定CPU概要信息的采樣頻率,并會(huì)在單獨(dú)的goroutine中運(yùn)行CPU概要信息的收集和輸出。StartCPUProfile函數(shù)設(shè)定的采樣頻率總是固定的100Hz,就是每秒采樣100次,或者說每10毫秒采樣一次。
關(guān)于CPU的主頻
CPU的主頻是CPU內(nèi)核工作的時(shí)鐘頻率,也常被稱為:CPU clock speed。這個(gè)時(shí)鐘頻率的倒數(shù)即為時(shí)鐘周期(clock cycle),也就是一個(gè)CPU內(nèi)核執(zhí)行一條運(yùn)算指令所需的時(shí)間,單位秒。例如:主頻為1000Hz的CPU,它的單個(gè)內(nèi)核執(zhí)行一條運(yùn)算指令所需的時(shí)間為0.001秒,即1毫秒。又例如,現(xiàn)在常見的3.2GHz的多核CPU,其單個(gè)內(nèi)核在1納秒的時(shí)間里就可以至少執(zhí)行三條運(yùn)算指令。
采樣頻率設(shè)定的原因
StartCPUProfile函數(shù)設(shè)定的CPU概要信息采樣頻率,相對(duì)于現(xiàn)代的CPU主頻來說是非常低的。這主要有兩個(gè)方面的原因。
一、過高的采樣頻率會(huì)對(duì)Go程序的運(yùn)行效率造成很明顯的負(fù)面影響。因此,runtime包中StartCPUProfileRate函數(shù)在被調(diào)用的時(shí)候,會(huì)保證采樣頻率不超過1MHz,也就是只允許1微妙最多采樣一次。StartCPUProfile函數(shù)正是通過調(diào)用這個(gè)函數(shù)來設(shè)定CPU概要信息的采樣頻率的。
二、經(jīng)過大量的實(shí)現(xiàn),GO語言團(tuán)隊(duì)發(fā)現(xiàn)100Hz是一個(gè)比較合適的設(shè)定。因?yàn)檫@樣做既可以得到足夠多、足夠有用的概要信息,又不至于讓程序的運(yùn)行出現(xiàn)停滯。另外,操作系統(tǒng)對(duì)高頻采樣的處理能力也是有限的,一般情況下,超過500Hz就很可能得不到及時(shí)的響應(yīng)的。
在StartCPUProfile函數(shù)執(zhí)行之后,一個(gè)新啟用的goroutine將會(huì)負(fù)責(zé)執(zhí)行CPU概要信息的收集和輸出,直到runtime/pprof包中的StopCPUProfile函數(shù)被成功調(diào)用。
StopCPUProfile函數(shù)也會(huì)調(diào)用runtime.SetCPUProfileRate函數(shù),并把參數(shù)值就是采樣頻率設(shè)為0。這會(huì)讓針對(duì)CPU概要信息的采樣工作停止。同時(shí)還會(huì)給負(fù)責(zé)收集CPU概要信息的代碼一個(gè)信號(hào),告知收集工作也需要停止。在接到信號(hào)之后,那部分程序?qū)?huì)把這段時(shí)間內(nèi)收集到的所有CPU概要信息,全部寫入到我們在調(diào)用StartCPUProfile函數(shù)的時(shí)候指定的寫入器中。只有在上述操作全部完成之后,StopCPUProfile函數(shù)才會(huì)返回。
上面已經(jīng)分析了,首先要調(diào)用StartCPUProfile函數(shù),要停止的時(shí)候就調(diào)用StopCPUProfile函數(shù)。中間就是需要進(jìn)行測試的代碼:
func main() {
// 打開文件,準(zhǔn)備寫入
filename := "cpuprofile2.out"
f, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
return
}
defer f.Close()
// 進(jìn)行采樣
if err := startCPUProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "CPU profile start error: %v\n", err)
return
}
/* 這里寫需要測試的代碼
*/
// 停止采樣
stopCPUProfile()
}
func startCPUProfile(w io.Writer) error {
if w == nil {
return errors.New("nil File")
}
return pprof.StartCPUProfile(w)
}
func stopCPUProfile() {
pprof.StopCPUProfile()
}
被測試的代碼
下面這段程序,應(yīng)該就是純粹為了看效果,是一段CPU密集型操作的代碼:
// article48/common/op/cpu.go
package op
import (
"bytes"
"math/rand"
"strconv"
)
func CPUProfile() error {
max := 10000000
var buf bytes.Buffer
for i := 0; i < max; i++ {
num := rand.Int63n(int64(max))
str := strconv.FormatInt(num, 10)
buf.WriteString(str)
}
_ = buf.String()
return nil
}
包裝被測試的函數(shù)
這里再額外做一步,對(duì)上面的函數(shù)進(jìn)行一次包裝,可以執(zhí)行多次被測試的函數(shù)。所以下面要實(shí)現(xiàn)的函數(shù)要傳入兩個(gè)參數(shù),一個(gè)是被測試的函數(shù),一個(gè)是希望執(zhí)行的次數(shù):
// article48/common/common.go
package common
import (
"errors"
"fmt"
"time"
)
// 代表包含高負(fù)載操作的函數(shù)
type OpFunc func() error
func Execute(op OpFunc, times int) (err error) {
if op == nil {
return errors.New("操作函數(shù)為nil")
}
if times <= 0 {
return fmt.Errorf("執(zhí)行次數(shù)不可用: %d", times)
}
var startTime time.Time
defer func() {
diff := time.Now().Sub(startTime)
fmt.Printf("執(zhí)行持續(xù)時(shí)間: %s\n", diff)
if p := recover(); p != nil {
err = fmt.Errorf("fatal error: %v", p)
}
}()
startTime = time.Now()
for i := 0; i < times; i++ {
if err = op(); err != nil {
return
}
time.Sleep(time.Microsecond)
}
return
}
這個(gè)函數(shù)是要準(zhǔn)備復(fù)用的。之后還會(huì)進(jìn)行內(nèi)存概要和阻塞概要的測試,也會(huì)有對(duì)應(yīng)的測試代碼。不過函數(shù)的簽名都將是一樣的:type OpFunc func() error
。
上面已經(jīng)有了完整的被測試函數(shù),以及包裝被測試函數(shù)的函數(shù)。這里把之前不完整的采樣測試的代碼再補(bǔ)充完整:
package main
import (
"Go36/article48/common"
"Go36/article48/common/op"
"errors"
"fmt"
"io"
"os"
"runtime/pprof"
)
func main() {
// 打開文件,準(zhǔn)備寫入
filename := "cpuprofile.out"
f, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
return
}
defer f.Close()
// 進(jìn)行采樣
if err := startCPUProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "CPU profile start error: %v\n", err)
return
}
// 被測試的函數(shù)
if err := common.Execute(op.CPUProfile, 10); err != nil {
fmt.Fprintf(os.Stderr, "execute error: %v\n", err)
return
}
// 停止采樣
stopCPUProfile()
}
func startCPUProfile(w io.Writer) error {
if w == nil {
return errors.New("nil File")
}
return pprof.StartCPUProfile(w)
}
func stopCPUProfile() {
pprof.StopCPUProfile()
}
現(xiàn)在可以執(zhí)行上面的程序,生成性能分析報(bào)告:
PS H:\Go\src\Go36\article48\example01> go run main.go
執(zhí)行持續(xù)時(shí)間: 8.3462144s
PS H:\Go\src\Go36\article48\example01>
執(zhí)行后會(huì)生成一個(gè)二進(jìn)制文件,需要用go tool pprof來查看
PS H:\Go\src\Go36\article48\example01> go tool pprof cpuprofile.out
Type: cpu
Time: Feb 12, 2019 at 7:33pm (CST)
Duration: 8.45s, Total samples = 8.50s (100.59%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof)
針對(duì)內(nèi)存概要信息的采樣會(huì)按照一點(diǎn)比例收集Go程序在運(yùn)行期間的堆內(nèi)存使用情況。
設(shè)定內(nèi)存概要信息采樣頻率的方法很簡單,只要為runtime.MemProfileRate變量賦值即可。
這個(gè)變量的含義是,平均每分配多少個(gè)字節(jié),就對(duì)堆內(nèi)存的使用情況進(jìn)行一次采樣。如果把該變量的值設(shè)為0,那么,Go語言運(yùn)行時(shí)系統(tǒng)就會(huì)完全停止對(duì)內(nèi)存概要信息的采樣。該變量的缺省值是512KB,即512千字節(jié)。如果要設(shè)定這個(gè)采樣頻率,就要越早越好,并且只應(yīng)該設(shè)定一次,否則就可能會(huì)對(duì)采集工作造成不良影響。比如,只在main函數(shù)的開始處設(shè)定一次。
之后,要獲取內(nèi)存概要信息,還需要調(diào)用WriteHeapProfile函數(shù)。該函數(shù)會(huì)把收集好的內(nèi)存概要信息寫到指定的寫入器中。通過WriteHeapProfile函數(shù)得到的內(nèi)存概要信息并不是實(shí)時(shí)的,它是一個(gè)快照,是在最近一次的內(nèi)存垃圾收集工作完成時(shí)產(chǎn)生的。如果想要實(shí)時(shí)的信息,那么可以調(diào)用runtime.ReadMemStats函數(shù)。不過要特別注意,該函數(shù)會(huì)引起Go語言調(diào)度器的短暫停頓。
復(fù)用之前的common程序,這里需要一個(gè)會(huì)分配很多內(nèi)存的測試代碼:
// article48/common/op/cpu.go
package op
import (
"bytes"
"encoding/json"
"math/rand"
)
// box 代表數(shù)據(jù)盒子。
type box struct {
Str string
Code rune
Bytes []byte
}
func MemProfile() error {
max := 50000
var buf bytes.Buffer
for j := 0; j < max; j++ {
seed := rand.Intn(95) + 32
one := createBox(seed)
b, err := genJSON(one)
if err != nil {
return err
}
buf.Write(b)
buf.WriteByte('\t')
}
_ = buf.String()
return nil
}
func createBox(seed int) box {
if seed <= 0 {
seed = 1
}
var array []byte
size := seed * 8
for i := 0; i < size; i++ {
array = append(array, byte(seed))
}
return box{
Str: string(seed),
Code: rune(seed),
Bytes: array,
}
}
func genJSON(one box) ([]byte, error) {
return json.Marshal(one)
}
用下面的示例來運(yùn)行這個(gè)測試:
package main
import (
"errors"
"fmt"
"os"
"Go36/article48/common"
"Go36/article48/common/op"
"runtime"
"runtime/pprof"
)
var memProfileRate = 8
func main() {
filename := "memprofile.out"
f, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
return
}
defer f.Close()
startMemProfile()
if err := common.Execute(op.MemProfile, 10); err != nil {
fmt.Fprintf(os.Stderr, "execute error: %v\n", err)
return
}
if err := stopMemProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "memory profile stop error: %v\n", err)
return
}
}
func startMemProfile() {
runtime.MemProfileRate = memProfileRate
}
func stopMemProfile(f *os.File) error {
if f == nil {
return errors.New("nil file")
}
return pprof.WriteHeapProfile(f)
}
調(diào)用SetBlockProfileRate函數(shù),即可對(duì)阻塞概要信息的采樣頻率進(jìn)行設(shè)定。
SetBlockProfileRate函數(shù)的參數(shù)rate是int類型。這個(gè)參數(shù)的含義是,只要發(fā)現(xiàn)一個(gè)阻塞事件的持續(xù)時(shí)間達(dá)到了rate納秒,就可以對(duì)其進(jìn)行采樣。如果這個(gè)參數(shù)的值小于或等于0,就會(huì)完全停止對(duì)阻塞概要信息的采樣。
另外還有一個(gè)blockprofilerate的包級(jí)私有變量uint64類型。這個(gè)變量的含義是,只要發(fā)現(xiàn)一個(gè)阻塞事件的持續(xù)時(shí)間跨越了多少個(gè)CPU時(shí)鐘周期,就可以對(duì)其進(jìn)行采樣。這個(gè)變量的值是自動(dòng)的通過rate參數(shù)來進(jìn)行設(shè)置的。
這兩個(gè)變量的區(qū)別僅僅是單位不同。SetBlockProfileRate函數(shù)會(huì)先對(duì)參數(shù)的rate值進(jìn)行單位換算和必要的類型轉(zhuǎn)換,然后,把換算的結(jié)果用原子操作賦值給blockprofilerate變量。由于此變量的缺省值是0,所以默認(rèn)情況下不記錄任何阻塞事件。
在需要獲取阻塞概要信息的時(shí)候,要先調(diào)用Lookup函數(shù),函數(shù)源碼如下:
func Lookup(name string) *Profile {
lockProfiles()
defer unlockProfiles()
return profiles.m[name]
}
這個(gè)函數(shù)下面會(huì)再詳細(xì)講,目前只要傳入"block"作為參數(shù)值。這里的"block"代表因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息,就是阻塞概要信息。該函數(shù)調(diào)用后會(huì)得到一個(gè)*Profile類型的值,就是Profile值。在這之后還需要調(diào)用這個(gè)Profile值的WriteTo方法,以驅(qū)使它把概要信息寫進(jìn)指定的寫入器中。
這個(gè)WriteTo方法有兩個(gè)參數(shù),源碼比較長,截取簽名的部分:
func (p *Profile) WriteTo(w io.Writer, debug int) error {
// 省略程序?qū)嶓w
}
第一個(gè)參數(shù)是寫入器,而第二個(gè)參數(shù)是代表概要信息詳細(xì)程度的int類型參數(shù)debug。debug參數(shù)的可選值有三個(gè),0、1或2:
用下面的函數(shù)來測試阻塞:
package op
import (
"math/rand"
"sync"
"time"
)
func BlockProfile() error {
max := 100
senderNum := max / 2
receiverNum := max / 4
ch2 := make(chan int, max/4)
var senderGroup sync.WaitGroup
senderGroup.Add(senderNum)
repeat := 50000
for j := 0; j < senderNum; j++ {
go send(ch2, &senderGroup, repeat)
}
go func() {
senderGroup.Wait()
close(ch2)
}()
var receiverGroup sync.WaitGroup
receiverGroup.Add(receiverNum)
for j := 0; j < receiverNum; j++ {
go receive(ch2, &receiverGroup)
}
receiverGroup.Wait()
return nil
}
func send(ch2 chan int, wg *sync.WaitGroup, repeat int) {
defer wg.Done()
time.Sleep(time.Millisecond * 10)
for k := 0; k < repeat; k++ {
elem := rand.Intn(repeat)
ch2 <- elem
}
}
func receive(ch2 chan int, wg *sync.WaitGroup) {
defer wg.Done()
for elem := range ch2 {
_ = elem
}
}
運(yùn)行下面的示例中的代碼,可以生成阻塞概要文件:
package main
import (
"errors"
"fmt"
"os"
"Go36/article48/common"
"Go36/article48/common/op"
"runtime"
"runtime/pprof"
)
var (
blockProfileRate = 2
debug = 0
)
func main() {
filename := "blockprofile.out"
f, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
return
}
defer f.Close()
startBlockProfile()
if err := common.Execute(op.BlockProfile, 10); err != nil {
fmt.Fprintf(os.Stderr, "execute error: %v\n", err)
return
}
if err := stopBlockProfile(f); err != nil {
fmt.Fprintf(os.Stderr, "block profile error: %v\n", err)
return
}
}
func startBlockProfile() {
runtime.SetBlockProfileRate(blockProfileRate)
}
func stopBlockProfile(f *os.File) error {
if f == nil {
return errors.New("nil file")
}
return pprof.Lookup("block").WriteTo(f, debug)
}
這里討論debug為2時(shí)的情況,此時(shí)就要根據(jù)Lookup函數(shù)的參數(shù)值來決定輸出的細(xì)節(jié)內(nèi)容了。
Lookup函數(shù)的功能是,提供與給定的名稱相對(duì)應(yīng)的概要信息。這個(gè)概要信息會(huì)由一個(gè)Profile值代表。如果該函數(shù)返回一個(gè)nil,那么就說明不存在與給定名稱對(duì)應(yīng)的概要信息。runtime/pprof包已經(jīng)預(yù)先定義了6個(gè)概要名稱。它們對(duì)應(yīng)的概要信息收集方法和輸出方法也都已經(jīng)準(zhǔn)備好了。這里直接拿來使用就可以了,把預(yù)定義好的名稱傳給name參數(shù)。具體是下面這些:
// goroutine - stack traces of all current goroutines
// heap - a sampling of memory allocations of live objects
// allocs - a sampling of all past memory allocations
// threadcreate - stack traces that led to the creation of new OS threads
// block - stack traces that led to blocking on synchronization primitives
// mutex - stack traces of holders of contended mutexes
收集當(dāng)前正在使用的所有g(shù)oroutine的堆棧跟蹤信息。注意,這樣的收集會(huì)引起Go語言調(diào)度器的短暫停頓。
調(diào)用該函數(shù)返回的Profile值的WriteTo方法時(shí),如果參數(shù)debug的值大于或等于2,那么該方法就會(huì)輸出所有g(shù)oroutine的堆棧跟蹤信息。這些信息可能會(huì)非常多。如果它們占用的空間超過了64M,那么相應(yīng)的方法就會(huì)將超出的部分截掉。
收集與堆內(nèi)存的分配和釋放有關(guān)的采樣信息。實(shí)際就是之前討論的內(nèi)存概要信息。
Lookup函數(shù)返回的Profile值的WriteTo方法被調(diào)用時(shí),輸出的內(nèi)存概要信息默認(rèn)以“在用空間”(inuse_space)的視角呈現(xiàn)。
在用空間,指已經(jīng)被分配但還未被釋放的內(nèi)存空間。在這個(gè)視角下,go tool pprof工具并不會(huì)去理會(huì)已釋放空間有關(guān)的那部分信息。
和上面的heap非常相似,也是收集與堆內(nèi)存的分配和釋放有關(guān)的采樣信息,就是內(nèi)存概要信息。
Lookup函數(shù)返回的Profile值的WriteTo方法被調(diào)用時(shí),輸出的內(nèi)存概要信息默認(rèn)以“已分配空間”(alloc_space)的視角呈現(xiàn)。
已分配空間,是所有的內(nèi)存分配信息都會(huì)被呈現(xiàn)出來,無論這些內(nèi)存空間在采樣時(shí)是否已經(jīng)被釋放。
與heap的差別
差別只是debug參數(shù)為0時(shí),WriteTo方法輸出的概要信息會(huì)有細(xì)微的差別。如果debug大于0,那么輸出的內(nèi)容是完全相同的。
收集堆棧跟蹤信息時(shí),這些堆棧跟蹤信息中的每一個(gè)都會(huì)描繪出一個(gè)代碼調(diào)用鏈,這些調(diào)用鏈上的代碼都導(dǎo)致新的操作系統(tǒng)線程產(chǎn)生。這樣的Profile值的輸出規(guī)格只有兩種,取決于WriteTo方法的debug參數(shù)是否大于0。
是因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息。就是之前討論的阻塞概要信息。這里輸出規(guī)格只有兩種,取決于debug是否大于0。
是曾經(jīng)作為同步原語持有者的那些代碼,它們的堆棧跟蹤信息。輸出規(guī)格也只有兩種,取決于debug是否大于0。
同步原語
這里所說的同步原語,指的是存在于Go語言運(yùn)行時(shí)系統(tǒng)內(nèi)部的一種底層的同步工具,或者說一種同步機(jī)制。它是直接面向內(nèi)存地址的,并以異步信號(hào)量和原子操作作為實(shí)現(xiàn)手段。通道、互斥鎖、條件變量、WatiGroup,以及Go語言運(yùn)行時(shí)系統(tǒng)本身,都會(huì)利用它來實(shí)現(xiàn)自己的功能。
在之前的測試代碼的基礎(chǔ)上,下面分別調(diào)用Lookup函數(shù)的每一個(gè)參數(shù)并且分別在debug是0、1、2時(shí)各執(zhí)行了一次,生成了所有可能的概要信息的文件:
package main
import (
"Go36/article48/common"
"Go36/article48/common/op"
"fmt"
"os"
"runtime"
"runtime/pprof"
"time"
)
// profileNames 代表概要信息名稱的列表。
var profileNames = []string{
"goroutine",
"heap",
"allocs",
"threadcreate",
"block",
"mutex",
}
// profileOps 代表為了生成不同的概要信息而準(zhǔn)備的負(fù)載函數(shù)的字典。
var profileOps = map[string]common.OpFunc{
"goroutine": op.BlockProfile,
"heap": op.MemProfile,
"allocs": op.MemProfile,
"threadcreate": op.BlockProfile,
"block": op.BlockProfile,
"mutex": op.BlockProfile,
}
// debugOpts 代表debug參數(shù)的可選值列表。
var debugOpts = []int{
0,
1,
2,
}
func main() {
prepare()
for _, name := range profileNames {
for _, debug := range debugOpts {
err := genProfile(name, debug)
if err != nil {
return
}
time.Sleep(time.Millisecond)
}
}
}
func genProfile(name string, debug int) error {
fmt.Printf("Generate %s profile (debug: %d) ...\n", name, debug)
filename := fmt.Sprintf("%s_%d.out", name, debug)
f, err := os.Create(filename)
if err != nil {
fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
return err
}
defer f.Close()
if err = common.Execute(profileOps[name], 10); err != nil {
fmt.Fprintf(os.Stderr, "execute error: %v (%s)\n", err, filename)
return err
}
profile := pprof.Lookup(name)
err = profile.WriteTo(f, debug)
if err != nil {
fmt.Fprintf(os.Stderr, "write error: %v (%s)\n", err, filename)
return err
}
return nil
}
func prepare() {
runtime.MemProfileRate = 8
runtime.SetBlockProfileRate(2)
}
針對(duì)上層的應(yīng)用,為基與HTTP協(xié)議的網(wǎng)絡(luò)服務(wù),添加性能分析接口。
這里做的是為之前的性能分析提供Web的瀏覽接口。上面生成的性能分析報(bào)告需要通過文件瀏覽器訪問文本內(nèi)容。通過這里的Web接口,則直接開啟一個(gè)Web服務(wù),直接用瀏覽器訪問來瀏覽各種性能分析報(bào)告。
在一般情況下只要在程序中導(dǎo)入net/http/pprof包就可以了:
import _ "net/http/pprof"
然后啟動(dòng)網(wǎng)絡(luò)服務(wù)并開始監(jiān)聽:
log.Println(http.ListenAndServe("localhost:8082", nil))
在運(yùn)行這個(gè)程序之后,就可以在瀏覽器中訪問下面的地址:
http://localhost:8082/debug/pprof
訪問后會(huì)得到一個(gè)簡約的網(wǎng)頁。點(diǎn)擊不同的連接,可以看到各種概要信息,這里自動(dòng)就生成所有種類的概要信息了。
debug參數(shù)
每個(gè)子路徑點(diǎn)進(jìn)去就會(huì)看到這個(gè)種類的概要信息。這里url還有一個(gè)debug參數(shù),這就是之前所講的WriteTo方法里的debug參數(shù)。默認(rèn)點(diǎn)進(jìn)去都是1,可以改成別的參數(shù)。如果是2就是詳細(xì)信息。如果是0就是二進(jìn)制信息,這時(shí)是無法瀏覽的,而是會(huì)觸發(fā)下載。
gc參數(shù)
另外還可以給url傳一個(gè)gc參數(shù),效果是控制是否在獲取概要信息之前強(qiáng)制執(zhí)行一次垃圾回收。只要它的值大于0,程序就會(huì)這樣做。不過,這個(gè)參數(shù)僅對(duì)heap有效,就是僅在/debug/pprof/heap路徑下有效。
一旦/debug/pprof/profile路徑被訪問,程序就會(huì)去執(zhí)行對(duì)CPU概要信息的采樣。它接受一個(gè)seconds的查詢參數(shù),就是采樣工作需要持續(xù)多少秒。如果參數(shù)未被顯式指定,那么采樣工作會(huì)持續(xù)30秒。所以一旦點(diǎn)下該連接,就會(huì)卡住,直到完成采樣。
另外,這里只會(huì)響應(yīng)經(jīng)protocol buffers轉(zhuǎn)換的字節(jié)流,所以采樣完成后,會(huì)觸發(fā)下載。另外還可以通過go tool pprof工具直接讀取這樣的HTTP響應(yīng):
go tool pprof http://localhost:8082/debug/pprof/profile?seconds=60
這個(gè)Web頁面還有一個(gè)路徑,/debug/pprof/trace。在這個(gè)路徑下,程序主要會(huì)利用runtime/trace包中的API來處理請(qǐng)求。
程序會(huì)先調(diào)用trace.Start函數(shù),然后在查詢參數(shù)seconds指定的持續(xù)時(shí)間之后再調(diào)用trace.Stop函數(shù)。這里的seconds的缺省值是1秒。而runtime/trace包的功用并沒有展開。
還可以定制URL,下面是一個(gè)定制的示例:
package main
import (
"log"
"net/http"
"net/http/pprof"
"strings"
)
func main() {
mux := http.NewServeMux()
pathPrefix := "/d/pprof/"
mux.HandleFunc(pathPrefix,
func(w http.ResponseWriter, r *http.Request) {
name := strings.TrimPrefix(r.URL.Path, pathPrefix)
if name != "" {
pprof.Handler(name).ServeHTTP(w, r)
return
}
pprof.Index(w, r)
})
mux.HandleFunc(pathPrefix+"cmdline", pprof.Cmdline)
mux.HandleFunc(pathPrefix+"profile", pprof.Profile)
mux.HandleFunc(pathPrefix+"symbol", pprof.Symbol)
mux.HandleFunc(pathPrefix+"trace", pprof.Trace)
server := http.Server{
Addr: "localhost:8083",
Handler: mux,
}
if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
log.Println("HTTP server closed.")
} else {
log.Printf("HTTP server error: %v\n", err)
}
}
}
在這里例子中,定制mux的代碼與包中的init函數(shù)很類型。默認(rèn)的路徑就是在init函數(shù)里實(shí)現(xiàn)的。并且之前直接用占位符導(dǎo)入net/http/pprof包的時(shí)候,就是執(zhí)行這個(gè)init函數(shù)而生成了默認(rèn)的訪問路徑。
在這里,使用net/http/pprof包要比直接使用runtime/pprof包方便和實(shí)用很多。通過合理運(yùn)用,這個(gè)代碼包可以為網(wǎng)絡(luò)服務(wù)的監(jiān)測提供有力的支撐。