Go36-48,49-程序性能分析基礎

程序性能分析基礎

本篇講的是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語言中,用於分析程序性能的概要文件有三種:

  1. CPU Profile : CPU概要文件
  2. Men Profile : 內存概要文件
  3. 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查看幫助信息。

Protocol Buffers

概要文件中的信息並不是普通的文本。而是通過protocol buffers生成的二進制數據流,或者說字節流。而protocol buffers是一種數據序列化協議,同時也是一個序列化工具。它可以把一個值,比如一個結構體或者一個字典,轉換成一段字節流。這個過程叫序列化。也可以反過來,把生成的字節流轉換爲程序中的一個值,這叫反序列化。
Go語言從1.8版本開始,把所有的profile相關的信息生成工作都交給protocol buffers來做了。它有不少的優勢。可以在序列化數據的同時對數據進行壓縮,所以生成的字節流通常都要比其他格式(XML和JSON)佔用的空間小很多。還支持自定義數據序列化和結構化的格式,也允許在保證向後兼容的前提下更新這種格式。這就是概要文件不使用普通文本格式保存的原因。
順便提一下,protocol buffers的用途非常廣泛,並且在諸如數據存儲、數據傳輸等任務中有着很高的使用率。

Protocol Buffers,是Google公司開發的一種數據描述語言,類似於XML能夠將結構化數據序列化,可用於數據存儲、通信協議等方面。

更多相關的知識就不展開的。

CPU概要信息

採樣CPU概要信息,需要用到runtime/pprof包中的API。要讓程序開始對CPU概要信息進行採樣,需要調用包中的StartCPUProfile函數。而在停止採樣的時候,需要調用包中的StopCPUProfile函數。

StartCPUProfile函數

runtime/pprof.StartCPUProfile函數在被調用的時候,先會去設定CPU概要信息的採樣頻率,並會在單獨的goroutine中運行CPU概要信息的收集和輸出。StartCPUProfile函數設定的採樣頻率總是固定的100Hz,就是每秒採樣100次,或者說每10毫秒採樣一次。

關於CPU的主頻
CPU的主頻是CPU內核工作的時鐘頻率,也常被稱爲:CPU clock speed。這個時鐘頻率的倒數即爲時鐘週期(clock cycle),也就是一個CPU內核執行一條運算指令所需的時間,單位秒。例如:主頻爲1000Hz的CPU,它的單個內核執行一條運算指令所需的時間爲0.001秒,即1毫秒。又例如,現在常見的3.2GHz的多核CPU,其單個內核在1納秒的時間裏就可以至少執行三條運算指令。

採樣頻率設定的原因
StartCPUProfile函數設定的CPU概要信息採樣頻率,相對於現代的CPU主頻來說是非常低的。這主要有兩個方面的原因。
一、過高的採樣頻率會對Go程序的運行效率造成很明顯的負面影響。因此,runtime包中StartCPUProfileRate函數在被調用的時候,會保證採樣頻率不超過1MHz,也就是隻允許1微妙最多采樣一次。StartCPUProfile函數正是通過調用這個函數來設定CPU概要信息的採樣頻率的。
二、經過大量的實現,GO語言團隊發現100Hz是一個比較合適的設定。因爲這樣做既可以得到足夠多、足夠有用的概要信息,又不至於讓程序的運行出現停滯。另外,操作系統對高頻採樣的處理能力也是有限的,一般情況下,超過500Hz就很可能得不到及時的響應的。

StopCPUProfile函數

在StartCPUProfile函數執行之後,一個新啓用的goroutine將會負責執行CPU概要信息的收集和輸出,直到runtime/pprof包中的StopCPUProfile函數被成功調用。
StopCPUProfile函數也會調用runtime.SetCPUProfileRate函數,並把參數值就是採樣頻率設爲0。這會讓針對CPU概要信息的採樣工作停止。同時還會給負責收集CPU概要信息的代碼一個信號,告知收集工作也需要停止。在接到信號之後,那部分程序將會把這段時間內收集到的所有CPU概要信息,全部寫入到我們在調用StartCPUProfile函數的時候指定的寫入器中。只有在上述操作全部完成之後,StopCPUProfile函數纔會返回。

編寫採樣代碼

上面已經分析了,首先要調用StartCPUProfile函數,要停止的時候就調用StopCPUProfile函數。中間就是需要進行測試的代碼:

func main() {
    // 打開文件,準備寫入
    filename := "cpuprofile2.out"
    f, err := os.Create(filename)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
        return
    }
    defer f.Close()

    // 進行採樣
    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()
}

被測試的代碼
下面這段程序,應該就是純粹爲了看效果,是一段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
}

包裝被測試的函數
這裏再額外做一步,對上面的函數進行一次包裝,可以執行多次被測試的函數。所以下面要實現的函數要傳入兩個參數,一個是被測試的函數,一個是希望執行的次數:

// article48/common/common.go
package common

import (
    "errors"
    "fmt"
    "time"
)

// 代表包含高負載操作的函數
type OpFunc func() error

func Execute(op OpFunc, times int) (err error) {
    if op == nil {
        return errors.New("操作函數爲nil")
    }
    if times <= 0 {
        return fmt.Errorf("執行次數不可用: %d", times)
    }
    var startTime time.Time
    defer func() {
        diff := time.Now().Sub(startTime)
        fmt.Printf("執行持續時間: %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
}

這個函數是要準備複用的。之後還會進行內存概要和阻塞概要的測試,也會有對應的測試代碼。不過函數的簽名都將是一樣的:type OpFunc func() error

完成測試

上面已經有了完整的被測試函數,以及包裝被測試函數的函數。這裏把之前不完整的採樣測試的代碼再補充完整:

package main

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

func main() {
    // 打開文件,準備寫入
    filename := "cpuprofile.out"
    f, err := os.Create(filename)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Create File Error: %v", err)
        return
    }
    defer f.Close()

    // 進行採樣
    if err := startCPUProfile(f); err != nil {
        fmt.Fprintf(os.Stderr, "CPU profile start error: %v\n", err)
        return
    }
    // 被測試的函數
    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()
}

現在可以執行上面的程序,生成性能分析報告:

PS H:\Go\src\Go36\article48\example01> go run main.go
執行持續時間: 8.3462144s
PS H:\Go\src\Go36\article48\example01>

執行後會生成一個二進制文件,需要用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)

內存概要信息

針對內存概要信息的採樣會按照一點比例收集Go程序在運行期間的堆內存使用情況。

採樣頻率

設定內存概要信息採樣頻率的方法很簡單,只要爲runtime.MemProfileRate變量賦值即可。
這個變量的含義是,平均每分配多少個字節,就對堆內存的使用情況進行一次採樣。如果把該變量的值設爲0,那麼,Go語言運行時系統就會完全停止對內存概要信息的採樣。該變量的缺省值是512KB,即512千字節。如果要設定這個採樣頻率,就要越早越好,並且只應該設定一次,否則就可能會對採集工作造成不良影響。比如,只在main函數的開始處設定一次。
之後,要獲取內存概要信息,還需要調用WriteHeapProfile函數。該函數會把收集好的內存概要信息寫到指定的寫入器中。通過WriteHeapProfile函數得到的內存概要信息並不是實時的,它是一個快照,是在最近一次的內存垃圾收集工作完成時產生的。如果想要實時的信息,那麼可以調用runtime.ReadMemStats函數。不過要特別注意,該函數會引起Go語言調度器的短暫停頓。

內存測試函數

複用之前的common程序,這裏需要一個會分配很多內存的測試代碼:

// article48/common/op/cpu.go
package op

import (
    "bytes"
    "encoding/json"
    "math/rand"
)

// box 代表數據盒子。
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)
}

完成測試

用下面的示例來運行這個測試:

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

阻塞概要信息

調用SetBlockProfileRate函數,即可對阻塞概要信息的採樣頻率進行設定。

參數設置

SetBlockProfileRate函數的參數rate是int類型。這個參數的含義是,只要發現一個阻塞事件的持續時間達到了rate納秒,就可以對其進行採樣。如果這個參數的值小於或等於0,就會完全停止對阻塞概要信息的採樣。
另外還有一個blockprofilerate的包級私有變量uint64類型。這個變量的含義是,只要發現一個阻塞事件的持續時間跨越了多少個CPU時鐘週期,就可以對其進行採樣。這個變量的值是自動的通過rate參數來進行設置的。
這兩個變量的區別僅僅是單位不同。SetBlockProfileRate函數會先對參數的rate值進行單位換算和必要的類型轉換,然後,把換算的結果用原子操作賦值給blockprofilerate變量。由於此變量的缺省值是0,所以默認情況下不記錄任何阻塞事件。

獲取信息

在需要獲取阻塞概要信息的時候,要先調用Lookup函數,函數源碼如下:

func Lookup(name string) *Profile {
    lockProfiles()
    defer unlockProfiles()
    return profiles.m[name]
}

這個函數下面會再詳細講,目前只要傳入"block"作爲參數值。這裏的"block"代表因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息,就是阻塞概要信息。該函數調用後會得到一個*Profile類型的值,就是Profile值。在這之後還需要調用這個Profile值的WriteTo方法,以驅使它把概要信息寫進指定的寫入器中。
這個WriteTo方法有兩個參數,源碼比較長,截取簽名的部分:

func (p *Profile) WriteTo(w io.Writer, debug int) error {
    // 省略程序實體
}

第一個參數是寫入器,而第二個參數是代表概要信息詳細程度的int類型參數debug。debug參數的可選值有三個,0、1或2:

  • debug爲0,通過WriteTo方法寫進寫入器的概要信息僅會包含go tool pprof工具所需的內存地址,這些內存地址會以十六進制的形式展現出來。並且概要信息是二進制字節流。
  • debug爲1,相應的包名、函數名、源碼文件路徑、代碼行號等信息都會作爲註釋被加入進去。並且概要信息是普通文本。
  • debug爲2,應該還包括大於2的情況,輸出通常會包含更多的細節。至於具體是哪些細節內容,就要看Lookup函數傳入的參數值了。概要信息還是普通文本。

阻塞測試函數

用下面的函數來測試阻塞:

package op

import (
    "math/rand"
    "sync"
    "time"
)

func BlockProfile() error {
    max := 100
    senderNum := max / 2
    receiverNum := max / 4
    ch1 := make(chan int, max/4)

    var senderGroup sync.WaitGroup
    senderGroup.Add(senderNum)
    repeat := 50000
    for j := 0; j < senderNum; j++ {
        go send(ch1, &senderGroup, repeat)
    }

    go func() {
        senderGroup.Wait()
        close(ch1)
    }()

    var receiverGroup sync.WaitGroup
    receiverGroup.Add(receiverNum)
    for j := 0; j < receiverNum; j++ {
        go receive(ch1, &receiverGroup)
    }
    receiverGroup.Wait()
    return nil
}

func send(ch1 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)
        ch1 <- elem
    }
}

func receive(ch1 chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for elem := range ch1 {
        _ = elem
    }
}

完成測試

運行下面的示例中的代碼,可以生成阻塞概要文件:

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時的情況,此時就要根據Lookup函數的參數值來決定輸出的細節內容了。
Lookup函數的功能是,提供與給定的名稱相對應的概要信息。這個概要信息會由一個Profile值代表。如果該函數返回一個nil,那麼就說明不存在與給定名稱對應的概要信息。runtime/pprof包已經預先定義了6個概要名稱。它們對應的概要信息收集方法和輸出方法也都已經準備好了。這裏直接拿來使用就可以了,把預定義好的名稱傳給name參數。具體是下面這些:

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

收集當前正在使用的所有goroutine的堆棧跟蹤信息。注意,這樣的收集會引起Go語言調度器的短暫停頓。
調用該函數返回的Profile值的WriteTo方法時,如果參數debug的值大於或等於2,那麼該方法就會輸出所有goroutine的堆棧跟蹤信息。這些信息可能會非常多。如果它們佔用的空間超過了64M,那麼相應的方法就會將超出的部分截掉。

heap

收集與堆內存的分配和釋放有關的採樣信息。實際就是之前討論的內存概要信息。
Lookup函數返回的Profile值的WriteTo方法被調用時,輸出的內存概要信息默認以“在用空間”(inuse_space)的視角呈現。
在用空間,指已經被分配但還未被釋放的內存空間。在這個視角下,go tool pprof工具並不會去理會已釋放空間有關的那部分信息。

allocs

和上面的heap非常相似,也是收集與堆內存的分配和釋放有關的採樣信息,就是內存概要信息。
Lookup函數返回的Profile值的WriteTo方法被調用時,輸出的內存概要信息默認以“已分配空間”(alloc_space)的視角呈現。
已分配空間,是所有的內存分配信息都會被呈現出來,無論這些內存空間在採樣時是否已經被釋放。

與heap的差別
差別只是debug參數爲0時,WriteTo方法輸出的概要信息會有細微的差別。如果debug大於0,那麼輸出的內容是完全相同的。

threadcreate

收集堆棧跟蹤信息時,這些堆棧跟蹤信息中的每一個都會描繪出一個代碼調用鏈,這些調用鏈上的代碼都導致新的操作系統線程產生。這樣的Profile值的輸出規格只有兩種,取決於WriteTo方法的debug參數是否大於0。

block

是因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息。就是之前討論的阻塞概要信息。這裏輸出規格只有兩種,取決於debug是否大於0。

mutex

是曾經作爲同步原語持有者的那些代碼,它們的堆棧跟蹤信息。輸出規格也只有兩種,取決於debug是否大於0。

同步原語
這裏所說的同步原語,指的是存在於Go語言運行時系統內部的一種底層的同步工具,或者說一種同步機制。它是直接面向內存地址的,並以異步信號量和原子操作作爲實現手段。通道、互斥鎖、條件變量、WatiGroup,以及Go語言運行時系統本身,都會利用它來實現自己的功能。

生成各種概要信息

在之前的測試代碼的基礎上,下面分別調用Lookup函數的每一個參數並且分別在debug是0、1、2時各執行了一次,生成了所有可能的概要信息的文件:

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 代表爲了生成不同的概要信息而準備的負載函數的字典。
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參數的可選值列表。
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)
}

性能分析網絡接口

針對上層的應用,爲基與HTTP協議的網絡服務,添加性能分析接口。
這裏做的是爲之前的性能分析提供Web的瀏覽接口。上面生成的性能分析報告需要通過文件瀏覽器訪問文本內容。通過這裏的Web接口,則直接開啓一個Web服務,直接用瀏覽器訪問來瀏覽各種性能分析報告。

基本用法

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

import _ "net/http/pprof"

然後啓動網絡服務並開始監聽:

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

在運行這個程序之後,就可以在瀏覽器中訪問下面的地址:

http://localhost:8082/debug/pprof

訪問後會得到一個簡約的網頁。點擊不同的連接,可以看到各種概要信息,這裏自動就生成所有種類的概要信息了。

debug參數
每個子路徑點進去就會看到這個種類的概要信息。這裏url還有一個debug參數,這就是之前所講的WriteTo方法裏的debug參數。默認點進去都是1,可以改成別的參數。如果是2就是詳細信息。如果是0就是二進制信息,這時是無法瀏覽的,而是會觸發下載。

gc參數
另外還可以給url傳一個gc參數,效果是控制是否在獲取概要信息之前強制執行一次垃圾回收。只要它的值大於0,程序就會這樣做。不過,這個參數僅對heap有效,就是僅在/debug/pprof/heap路徑下有效。

CPU概要信息

一旦/debug/pprof/profile路徑被訪問,程序就會去執行對CPU概要信息的採樣。它接受一個seconds的查詢參數,就是採樣工作需要持續多少秒。如果參數未被顯式指定,那麼採樣工作會持續30秒。所以一旦點下該連接,就會卡住,直到完成採樣。
另外,這裏只會響應經protocol buffers轉換的字節流,所以採樣完成後,會觸發下載。另外還可以通過go tool pprof工具直接讀取這樣的HTTP響應:

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

runtime/trace

這個Web頁面還有一個路徑,/debug/pprof/trace。在這個路徑下,程序主要會利用runtime/trace包中的API來處理請求。
程序會先調用trace.Start函數,然後在查詢參數seconds指定的持續時間之後再調用trace.Stop函數。這裏的seconds的缺省值是1秒。而runtime/trace包的功用並沒有展開。

定製URL

還可以定製URL,下面是一個定製的示例:

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函數很類型。默認的路徑就是在init函數裏實現的。並且之前直接用佔位符導入net/http/pprof包的時候,就是執行這個init函數而生成了默認的訪問路徑。

小結

在這裏,使用net/http/pprof包要比直接使用runtime/pprof包方便和實用很多。通過合理運用,這個代碼包可以爲網絡服務的監測提供有力的支撐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章