Loading

Go语言精进之路读书笔记第47条——使用pprof对程序进行性能剖析

47.1 pprof的工作原理

1.采样数据类型

(1) CPU数据
(2) 堆内存分配数据
(3) 锁竞争数据
(4) 阻塞时间数据

2.性能数据采集的方式

(1) 通过性能基准测试进行数据采集

go test -bench . xxx_test.go -cpuprofile=cpu.prof
go test -bench . xxx_test.go -memprofile=mem.prof
go test -bench . xxx_test.go -mutexprofile=mutex.prof
go test -bench . xxx_test.go -blockprofile=block.prof

(2) 独立程序的性能数据采集

  • 可以通过标准库runtime/pprof和runtime包提供的低级API对独立程序进行性能数据采集。
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
var mutexprofile = flag.String("mutexprofile", "", "write mutex profile to `file`")
var blockprofile = flag.String("blockprofile", "", "write block profile to `file`")

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal("could not create CPU profile: ", err)
        }
        defer f.Close() // 该例子中暂忽略错误处理
        if err := pprof.StartCPUProfile(f); err != nil {
            log.Fatal("could not start CPU profile: ", err)
        }
        defer pprof.StopCPUProfile()
    }

    if *memprofile != "" {
        f, err := os.Create(*memprofile)
        if err != nil {
            log.Fatal("could not create memory profile: ", err)
        }
        defer f.Close()
        if err := pprof.WriteHeapProfile(f); err != nil {
            log.Fatal("could not write memory profile: ", err)
        }
    }

    if *mutexprofile != "" {
        runtime.SetMutexProfileFraction(1)
        defer runtime.SetMutexProfileFraction(0)
        f, err := os.Create(*mutexprofile)
        if err != nil {
            log.Fatal("could not create mutex profile: ", err)
        }
        defer f.Close()

        if mp := pprof.Lookup("mutex"); mp != nil {
            mp.WriteTo(f, 0)
        }
    }

    if *blockprofile != "" {
        runtime.SetBlockProfileRate(1)
        defer runtime.SetBlockProfileRate(0)
        f, err := os.Create(*blockprofile)
        if err != nil {
            log.Fatal("could not create block profile: ", err)
        }
        defer f.Close()

        if mp := pprof.Lookup("mutex"); mp != nil {
            mp.WriteTo(f, 0)
        }
    }

    var wg sync.WaitGroup
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    wg.Add(1)
    go func() {
        for {
            select {
            case <-c:
                wg.Done()
                return
            default:
                s1 := "hello,"
                s2 := "gopher"
                s3 := "!"
                _ = s1 + s2 + s3
            }

            time.Sleep(10 * time.Millisecond)
        }
    }()
    wg.Wait()
    fmt.Println("program exit")
}
  • Go在net/http/pprof包中还提供了一种更为高级的针对独立程序性能数据采集方式,尤其适合那些内置了HTTP服务的独立程序。
import (
    "context"
    "fmt"
    "net/http"
    _ "net/http/pprof"
    "os"
    "os/signal"
    "syscall"
)

func main() {
    http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(*r)
        w.Write([]byte("hello"))
    }))
    s := http.Server{
        Addr: "localhost:8080",
    }
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-c
        s.Shutdown(context.Background())
    }()
    fmt.Println(s.ListenAndServe())
}

3.性能数据的剖析

(1) 命令行交互方式

  • flat列的值表示函数自身代码在数据采样过程中的执行时长,flat%列的值表示该执行时长占总采样执行时长的百分比
  • sum%列的值是当前行flat%值与排在该值前面所有行的flat%值的累加和
  • cum列的值表示函数自身在数据采样过程中出现的时长,这个时长是其自身代码执行时长及其等待其调用的函数返回所用时长的总和。越是接近函数调用栈底层的代码,其cum列的值越大
  • cum%列的值表示该函数cum值占总采样时长的百分比

(2) Web图形化方式

  • Top视图等价于命令行交互模式下的topN命令输出
  • Source视图等价于命令行交互模式下的list命令输出
  • Flame Graph视图即火焰图
    • go tool pprof在浏览器中呈现出的火焰图与标准火焰图有些差异:它是倒置的,即调用栈最顶端的函数在最下方
    • 倒置火焰图就是看最下面的哪个函数占据的宽度最大,这样的函数可能存在性能问题

47.2 使用pprof进行性能剖析的实例

1.待优化程序(step0)

2.CPU类性能数据采样及数据剖析(step1)

3.第一次优化(step2)
优化手段:正则表达式仅编译一次

4.内存分配采样数据剖析

5.第二次优化(step3)

  • 删除w.Header().Set这行调用
  • 使用fmt.Fprintf替代w.Write

6.零内存分配(step4)

  • 使用sync.Pool减少重新分配bytes.Buffer次数
  • 采用预分配底层存储的bytes.Buffer拼接输出
  • 使用strconv.AppendInt将整型数拼接到bytes.Buffer中

7.查看并发下的阻塞情况(step5)

  • 无需处理,Go标准库对regexp包的Regexp.MatchString方法做过针对并发的优化(也是采用sync.Pool)
posted @ 2024-03-10 20:16  brynchen  阅读(20)  评论(0编辑  收藏  举报