您好,登錄后才能下訂單哦!
本篇講的是Go程序的性能分析,下面提到的內容都是從事這項任務必備的一些知識和技巧。這些有助于我們真正理解以采樣、收集、輸出為代表的一系列操作步驟。
Go語言為程序開發者們提供了豐富的性能分析API,和非常好用的標準工具。這些API主要存在于下面三個包中:
另外,runtime包中還包含了一些更底層的API。這些都可以被用來收集或輸出Go程序運行過程中的一些關鍵指標,并幫助我們生成相應的概要文件以供后續分析時使用。
標準工具主要有:
這兩個工具,可以解析概要文件中的信息,并以人類易讀的方式把這些信息展示出來。
go test命令,也可以在程序測試完成后生成概要文件。這樣就可以很方便的使用前面那兩個工具讀取概要文件,并對被測程序的性能加以分析。這樣就讓程序性能測試的資料更加豐富,結果也更加精確和可信。
在Go語言中,用于分析程序性能的概要文件有三種:
這些概要文件中包含的都是:在某一段時間內,對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是一種數據序列化協議,同時也是一個序列化工具。它可以把一個值,比如一個結構體或者一個字典,轉換成一段字節流。這個過程叫序列化。也可以反過來,把生成的字節流轉換為程序中的一個值,這叫反序列化。
Go語言從1.8版本開始,把所有的profile相關的信息生成工作都交給protocol buffers來做了。它有不少的優勢。可以在序列化數據的同時對數據進行壓縮,所以生成的字節流通常都要比其他格式(XML和JSON)占用的空間小很多。還支持自定義數據序列化和結構化的格式,也允許在保證向后兼容的前提下更新這種格式。這就是概要文件不使用普通文本格式保存的原因。
順便提一下,protocol buffers的用途非常廣泛,并且在諸如數據存儲、數據傳輸等任務中有著很高的使用率。
Protocol Buffers,是Google公司開發的一種數據描述語言,類似于XML能夠將結構化數據序列化,可用于數據存儲、通信協議等方面。
更多相關的知識就不展開的。
采樣CPU概要信息,需要用到runtime/pprof包中的API。要讓程序開始對CPU概要信息進行采樣,需要調用包中的StartCPUProfile函數。而在停止采樣的時候,需要調用包中的StopCPUProfile函數。
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就很可能得不到及時的響應的。
在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:
用下面的函數來測試阻塞:
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
}
}
運行下面的示例中的代碼,可以生成阻塞概要文件:
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的堆棧跟蹤信息。注意,這樣的收集會引起Go語言調度器的短暫停頓。
調用該函數返回的Profile值的WriteTo方法時,如果參數debug的值大于或等于2,那么該方法就會輸出所有goroutine的堆棧跟蹤信息。這些信息可能會非常多。如果它們占用的空間超過了64M,那么相應的方法就會將超出的部分截掉。
收集與堆內存的分配和釋放有關的采樣信息。實際就是之前討論的內存概要信息。
Lookup函數返回的Profile值的WriteTo方法被調用時,輸出的內存概要信息默認以“在用空間”(inuse_space)的視角呈現。
在用空間,指已經被分配但還未被釋放的內存空間。在這個視角下,go tool pprof工具并不會去理會已釋放空間有關的那部分信息。
和上面的heap非常相似,也是收集與堆內存的分配和釋放有關的采樣信息,就是內存概要信息。
Lookup函數返回的Profile值的WriteTo方法被調用時,輸出的內存概要信息默認以“已分配空間”(alloc_space)的視角呈現。
已分配空間,是所有的內存分配信息都會被呈現出來,無論這些內存空間在采樣時是否已經被釋放。
與heap的差別
差別只是debug參數為0時,WriteTo方法輸出的概要信息會有細微的差別。如果debug大于0,那么輸出的內容是完全相同的。
收集堆棧跟蹤信息時,這些堆棧跟蹤信息中的每一個都會描繪出一個代碼調用鏈,這些調用鏈上的代碼都導致新的操作系統線程產生。這樣的Profile值的輸出規格只有兩種,取決于WriteTo方法的debug參數是否大于0。
是因爭用同步原語而被阻塞的那些代碼的堆棧跟蹤信息。就是之前討論的阻塞概要信息。這里輸出規格只有兩種,取決于debug是否大于0。
是曾經作為同步原語持有者的那些代碼,它們的堆棧跟蹤信息。輸出規格也只有兩種,取決于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路徑下有效。
一旦/debug/pprof/profile路徑被訪問,程序就會去執行對CPU概要信息的采樣。它接受一個seconds的查詢參數,就是采樣工作需要持續多少秒。如果參數未被顯式指定,那么采樣工作會持續30秒。所以一旦點下該連接,就會卡住,直到完成采樣。
另外,這里只會響應經protocol buffers轉換的字節流,所以采樣完成后,會觸發下載。另外還可以通過go tool pprof工具直接讀取這樣的HTTP響應:
go tool pprof http://localhost:8082/debug/pprof/profile?seconds=60
這個Web頁面還有一個路徑,/debug/pprof/trace。在這個路徑下,程序主要會利用runtime/trace包中的API來處理請求。
程序會先調用trace.Start函數,然后在查詢參數seconds指定的持續時間之后再調用trace.Stop函數。這里的seconds的缺省值是1秒。而runtime/trace包的功用并沒有展開。
還可以定制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包方便和實用很多。通過合理運用,這個代碼包可以為網絡服務的監測提供有力的支撐。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。