溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Go36-48,49-程序性能分析基礎(chǔ)

發(fā)布時(shí)間:2020-07-15 08:15:21 來(lái)源:網(wǎng)絡(luò) 閱讀:1453 作者:騎士救兵 欄目:編程語(yǔ)言

程序性能分析基礎(chǔ)

本篇講的是Go程序的性能分析,下面提到的內(nèi)容都是從事這項(xiàng)任務(wù)必備的一些知識(shí)和技巧。這些有助于我們真正理解以采樣、收集、輸出為代表的一系列操作步驟。

代碼包

Go語(yǔ)言為程序開(kāi)發(fā)者們提供了豐富的性能分析API,和非常好用的標(biāo)準(zhǔn)工具。這些API主要存在于下面三個(gè)包中:

  1. runtime/pprof
  2. net/http/pprof
  3. runtime/trace

另外,runtime包中還包含了一些更底層的API。這些都可以被用來(lái)收集或輸出Go程序運(yùn)行過(guò)程中的一些關(guān)鍵指標(biāo),并幫助我們生成相應(yīng)的概要文件以供后續(xù)分析時(shí)使用。

標(biāo)準(zhǔn)工具

標(biāo)準(zhǔn)工具主要有:

  • go tool pprof
  • go tool trace

這兩個(gè)工具,可以解析概要文件中的信息,并以人類易讀的方式把這些信息展示出來(lái)。

go test命令,也可以在程序測(cè)試完成后生成概要文件。這樣就可以很方便的使用前面那兩個(gè)工具讀取概要文件,并對(duì)被測(cè)程序的性能加以分析。這樣就讓程序性能測(cè)試的資料更加豐富,結(jié)果也更加精確和可信。

概要文件

在Go語(yǔ)言中,用于分析程序性能的概要文件有三種:

  1. CPU Profile : CPU概要文件
  2. Men Profile : 內(nèi)存概要文件
  3. Block Profile : 阻塞概要文件

這些概要文件中包含的都是:在某一段時(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è)工具來(lái)查看。可以通過(guò)該工具進(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è)工具的具體用法沒(méi)有展開(kāi)。建議在使用時(shí),輸入help查看幫助信息。

Protocol Buffers

概要文件中的信息并不是普通的文本。而是通過(guò)protocol buffers生成的二進(jìn)制數(shù)據(jù)流,或者說(shuō)字節(jié)流。而protocol buffers是一種數(shù)據(jù)序列化協(xié)議,同時(shí)也是一個(gè)序列化工具。它可以把一個(gè)值,比如一個(gè)結(jié)構(gòu)體或者一個(gè)字典,轉(zhuǎn)換成一段字節(jié)流。這個(gè)過(guò)程叫序列化。也可以反過(guò)來(lái),把生成的字節(jié)流轉(zhuǎn)換為程序中的一個(gè)值,這叫反序列化。
Go語(yǔ)言從1.8版本開(kāi)始,把所有的profile相關(guān)的信息生成工作都交給protocol buffers來(lái)做了。它有不少的優(yōu)勢(shì)??梢栽谛蛄谢瘮?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公司開(kāi)發(fā)的一種數(shù)據(jù)描述語(yǔ)言,類似于XML能夠?qū)⒔Y(jié)構(gòu)化數(shù)據(jù)序列化,可用于數(shù)據(jù)存儲(chǔ)、通信協(xié)議等方面。

更多相關(guān)的知識(shí)就不展開(kāi)的。

CPU概要信息

采樣CPU概要信息,需要用到runtime/pprof包中的API。要讓程序開(kāi)始對(duì)CPU概要信息進(jìn)行采樣,需要調(diào)用包中的StartCPUProfile函數(shù)。而在停止采樣的時(shí)候,需要調(diào)用包中的StopCPUProfile函數(shù)。

StartCPUProfile函數(shù)

runtime/pprof.StartCPUProfile函數(shù)在被調(diào)用的時(shí)候,先會(huì)去設(shè)定CPU概要信息的采樣頻率,并會(huì)在單獨(dú)的goroutine中運(yùn)行CPU概要信息的收集和輸出。StartCPUProfile函數(shù)設(shè)定的采樣頻率總是固定的100Hz,就是每秒采樣100次,或者說(shuō)每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)在常見(jiàn)的3.2GHz的多核CPU,其單個(gè)內(nèi)核在1納秒的時(shí)間里就可以至少執(zhí)行三條運(yùn)算指令。

采樣頻率設(shè)定的原因
StartCPUProfile函數(shù)設(shè)定的CPU概要信息采樣頻率,相對(duì)于現(xiàn)代的CPU主頻來(lái)說(shuō)是非常低的。這主要有兩個(gè)方面的原因。
一、過(guò)高的采樣頻率會(huì)對(duì)Go程序的運(yùn)行效率造成很明顯的負(fù)面影響。因此,runtime包中StartCPUProfileRate函數(shù)在被調(diào)用的時(shí)候,會(huì)保證采樣頻率不超過(guò)1MHz,也就是只允許1微妙最多采樣一次。StartCPUProfile函數(shù)正是通過(guò)調(diào)用這個(gè)函數(shù)來(lái)設(shè)定CPU概要信息的采樣頻率的。
二、經(jīng)過(guò)大量的實(shí)現(xiàn),GO語(yǔ)言團(tuán)隊(duì)發(fā)現(xiàn)100Hz是一個(gè)比較合適的設(shè)定。因?yàn)檫@樣做既可以得到足夠多、足夠有用的概要信息,又不至于讓程序的運(yùn)行出現(xiàn)停滯。另外,操作系統(tǒng)對(duì)高頻采樣的處理能力也是有限的,一般情況下,超過(guò)500Hz就很可能得不到及時(shí)的響應(yīng)的。

StopCPUProfile函數(shù)

在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概要信息,全部寫(xiě)入到我們?cè)谡{(diào)用StartCPUProfile函數(shù)的時(shí)候指定的寫(xiě)入器中。只有在上述操作全部完成之后,StopCPUProfile函數(shù)才會(huì)返回。

編寫(xiě)采樣代碼

上面已經(jīng)分析了,首先要調(diào)用StartCPUProfile函數(shù),要停止的時(shí)候就調(diào)用StopCPUProfile函數(shù)。中間就是需要進(jìn)行測(cè)試的代碼:

func main() {
    // 打開(kāi)文件,準(zhǔn)備寫(xiě)入
    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
    }
    /* 這里寫(xiě)需要測(cè)試的代碼
    */
    // 停止采樣
    stopCPUProfile()
}

func startCPUProfile(w io.Writer) error {
    if w == nil {
        return errors.New("nil File")
    }
    return pprof.StartCPUProfile(w)
}

func stopCPUProfile() {
    pprof.StopCPUProfile()
}

被測(cè)試的代碼
下面這段程序,應(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
}

包裝被測(cè)試的函數(shù)
這里再額外做一步,對(duì)上面的函數(shù)進(jìn)行一次包裝,可以執(zhí)行多次被測(cè)試的函數(shù)。所以下面要實(shí)現(xiàn)的函數(shù)要傳入兩個(gè)參數(shù),一個(gè)是被測(cè)試的函數(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)存概要和阻塞概要的測(cè)試,也會(huì)有對(duì)應(yīng)的測(cè)試代碼。不過(guò)函數(shù)的簽名都將是一樣的:type OpFunc func() error。

完成測(cè)試

上面已經(jīng)有了完整的被測(cè)試函數(shù),以及包裝被測(cè)試函數(shù)的函數(shù)。這里把之前不完整的采樣測(cè)試的代碼再補(bǔ)充完整:

package main

import (
    "Go36/article48/common"
    "Go36/article48/common/op"
    "errors"
    "fmt"
    "io"
    "os"
    "runtime/pprof"
)

func main() {
    // 打開(kāi)文件,準(zhǔn)備寫(xiě)入
    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
    }
    // 被測(cè)試的函數(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來(lái)查看

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)

內(nèi)存概要信息

針對(duì)內(nèi)存概要信息的采樣會(huì)按照一點(diǎn)比例收集Go程序在運(yùn)行期間的堆內(nèi)存使用情況。

采樣頻率

設(shè)定內(nèi)存概要信息采樣頻率的方法很簡(jiǎn)單,只要為runtime.MemProfileRate變量賦值即可。
這個(gè)變量的含義是,平均每分配多少個(gè)字節(jié),就對(duì)堆內(nèi)存的使用情況進(jìn)行一次采樣。如果把該變量的值設(shè)為0,那么,Go語(yǔ)言運(yùn)行時(shí)系統(tǒng)就會(huì)完全停止對(duì)內(nèi)存概要信息的采樣。該變量的缺省值是512KB,即512千字節(jié)。如果要設(shè)定這個(gè)采樣頻率,就要越早越好,并且只應(yīng)該設(shè)定一次,否則就可能會(huì)對(duì)采集工作造成不良影響。比如,只在main函數(shù)的開(kāi)始處設(shè)定一次。
之后,要獲取內(nèi)存概要信息,還需要調(diào)用WriteHeapProfile函數(shù)。該函數(shù)會(huì)把收集好的內(nèi)存概要信息寫(xiě)到指定的寫(xiě)入器中。通過(guò)WriteHeapProfile函數(shù)得到的內(nèi)存概要信息并不是實(shí)時(shí)的,它是一個(gè)快照,是在最近一次的內(nèi)存垃圾收集工作完成時(shí)產(chǎn)生的。如果想要實(shí)時(shí)的信息,那么可以調(diào)用runtime.ReadMemStats函數(shù)。不過(guò)要特別注意,該函數(shù)會(huì)引起Go語(yǔ)言調(diào)度器的短暫停頓。

內(nèi)存測(cè)試函數(shù)

復(fù)用之前的common程序,這里需要一個(gè)會(huì)分配很多內(nèi)存的測(cè)試代碼:

// 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)
}

完成測(cè)試

用下面的示例來(lái)運(yùn)行這個(gè)測(cè)試:

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è)定。

參數(shù)設(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)的通過(guò)rate參數(shù)來(lái)進(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"代表因爭(zhēng)用同步原語(yǔ)而被阻塞的那些代碼的堆棧跟蹤信息,就是阻塞概要信息。該函數(shù)調(diào)用后會(huì)得到一個(gè)*Profile類型的值,就是Profile值。在這之后還需要調(diào)用這個(gè)Profile值的WriteTo方法,以驅(qū)使它把概要信息寫(xiě)進(jìn)指定的寫(xiě)入器中。
這個(gè)WriteTo方法有兩個(gè)參數(shù),源碼比較長(zhǎng),截取簽名的部分:

func (p *Profile) WriteTo(w io.Writer, debug int) error {
    // 省略程序?qū)嶓w
}

第一個(gè)參數(shù)是寫(xiě)入器,而第二個(gè)參數(shù)是代表概要信息詳細(xì)程度的int類型參數(shù)debug。debug參數(shù)的可選值有三個(gè),0、1或2:

  • debug為0,通過(guò)WriteTo方法寫(xiě)進(jìn)寫(xiě)入器的概要信息僅會(huì)包含go tool pprof工具所需的內(nèi)存地址,這些內(nèi)存地址會(huì)以十六進(jìn)制的形式展現(xiàn)出來(lái)。并且概要信息是二進(jìn)制字節(jié)流。
  • debug為1,相應(yīng)的包名、函數(shù)名、源碼文件路徑、代碼行號(hào)等信息都會(huì)作為注釋被加入進(jìn)去。并且概要信息是普通文本。
  • debug為2,應(yīng)該還包括大于2的情況,輸出通常會(huì)包含更多的細(xì)節(jié)。至于具體是哪些細(xì)節(jié)內(nèi)容,就要看Lookup函數(shù)傳入的參數(shù)值了。概要信息還是普通文本。

阻塞測(cè)試函數(shù)

用下面的函數(shù)來(lái)測(cè)試阻塞:

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
    }
}

完成測(cè)試

運(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ù)值來(lái)決定輸出的細(xì)節(jié)內(nèi)容了。
Lookup函數(shù)的功能是,提供與給定的名稱相對(duì)應(yīng)的概要信息。這個(gè)概要信息會(huì)由一個(gè)Profile值代表。如果該函數(shù)返回一個(gè)nil,那么就說(shuō)明不存在與給定名稱對(duì)應(yīng)的概要信息。runtime/pprof包已經(jīng)預(yù)先定義了6個(gè)概要名稱。它們對(duì)應(yīng)的概要信息收集方法和輸出方法也都已經(jīng)準(zhǔn)備好了。這里直接拿來(lái)使用就可以了,把預(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

goroutine

收集當(dāng)前正在使用的所有g(shù)oroutine的堆棧跟蹤信息。注意,這樣的收集會(huì)引起Go語(yǔ)言調(diào)度器的短暫停頓。
調(diào)用該函數(shù)返回的Profile值的WriteTo方法時(shí),如果參數(shù)debug的值大于或等于2,那么該方法就會(huì)輸出所有g(shù)oroutine的堆棧跟蹤信息。這些信息可能會(huì)非常多。如果它們占用的空間超過(guò)了64M,那么相應(yīng)的方法就會(huì)將超出的部分截掉。

heap

收集與堆內(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)的那部分信息。

allocs

和上面的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)出來(lái),無(wú)論這些內(nèi)存空間在采樣時(shí)是否已經(jīng)被釋放。

與heap的差別
差別只是debug參數(shù)為0時(shí),WriteTo方法輸出的概要信息會(huì)有細(xì)微的差別。如果debug大于0,那么輸出的內(nèi)容是完全相同的。

threadcreate

收集堆棧跟蹤信息時(shí),這些堆棧跟蹤信息中的每一個(gè)都會(huì)描繪出一個(gè)代碼調(diào)用鏈,這些調(diào)用鏈上的代碼都導(dǎo)致新的操作系統(tǒng)線程產(chǎn)生。這樣的Profile值的輸出規(guī)格只有兩種,取決于WriteTo方法的debug參數(shù)是否大于0。

block

是因爭(zhēng)用同步原語(yǔ)而被阻塞的那些代碼的堆棧跟蹤信息。就是之前討論的阻塞概要信息。這里輸出規(guī)格只有兩種,取決于debug是否大于0。

mutex

是曾經(jīng)作為同步原語(yǔ)持有者的那些代碼,它們的堆棧跟蹤信息。輸出規(guī)格也只有兩種,取決于debug是否大于0。

同步原語(yǔ)
這里所說(shuō)的同步原語(yǔ),指的是存在于Go語(yǔ)言運(yùn)行時(shí)系統(tǒng)內(nèi)部的一種底層的同步工具,或者說(shuō)一種同步機(jī)制。它是直接面向內(nèi)存地址的,并以異步信號(hào)量和原子操作作為實(shí)現(xiàn)手段。通道、互斥鎖、條件變量、WatiGroup,以及Go語(yǔ)言運(yùn)行時(shí)系統(tǒng)本身,都會(huì)利用它來(lái)實(shí)現(xiàn)自己的功能。

生成各種概要信息

在之前的測(cè)試代碼的基礎(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)
}

性能分析網(wǎng)絡(luò)接口

針對(duì)上層的應(yīng)用,為基與HTTP協(xié)議的網(wǎng)絡(luò)服務(wù),添加性能分析接口。
這里做的是為之前的性能分析提供Web的瀏覽接口。上面生成的性能分析報(bào)告需要通過(guò)文件瀏覽器訪問(wèn)文本內(nèi)容。通過(guò)這里的Web接口,則直接開(kāi)啟一個(gè)Web服務(wù),直接用瀏覽器訪問(wèn)來(lái)瀏覽各種性能分析報(bào)告。

基本用法

在一般情況下只要在程序中導(dǎo)入net/http/pprof包就可以了:

import _ "net/http/pprof"

然后啟動(dòng)網(wǎng)絡(luò)服務(wù)并開(kāi)始監(jiān)聽(tīng):

log.Println(http.ListenAndServe("localhost:8082", nil))

在運(yùn)行這個(gè)程序之后,就可以在瀏覽器中訪問(wèn)下面的地址:

http://localhost:8082/debug/pprof

訪問(wèn)后會(huì)得到一個(gè)簡(jiǎn)約的網(wǎng)頁(yè)。點(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í)是無(wú)法瀏覽的,而是會(huì)觸發(fā)下載。

gc參數(shù)
另外還可以給url傳一個(gè)gc參數(shù),效果是控制是否在獲取概要信息之前強(qiáng)制執(zhí)行一次垃圾回收。只要它的值大于0,程序就會(huì)這樣做。不過(guò),這個(gè)參數(shù)僅對(duì)heap有效,就是僅在/debug/pprof/heap路徑下有效。

CPU概要信息

一旦/debug/pprof/profile路徑被訪問(wèn),程序就會(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ā)下載。另外還可以通過(guò)go tool pprof工具直接讀取這樣的HTTP響應(yīng):

go tool pprof http://localhost:8082/debug/pprof/profile?seconds=60

runtime/trace

這個(gè)Web頁(yè)面還有一個(gè)路徑,/debug/pprof/trace。在這個(gè)路徑下,程序主要會(huì)利用runtime/trace包中的API來(lái)處理請(qǐng)求。
程序會(huì)先調(diào)用trace.Start函數(shù),然后在查詢參數(shù)seconds指定的持續(xù)時(shí)間之后再調(diào)用trace.Stop函數(shù)。這里的seconds的缺省值是1秒。而runtime/trace包的功用并沒(méi)有展開(kāi)。

定制URL

還可以定制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)的訪問(wèn)路徑。

小結(jié)

在這里,使用net/http/pprof包要比直接使用runtime/pprof包方便和實(shí)用很多。通過(guò)合理運(yùn)用,這個(gè)代碼包可以為網(wǎng)絡(luò)服務(wù)的監(jiān)測(cè)提供有力的支撐。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI