go应用程序性能分析

1 介绍

pprof 是一个可视化和分析数据的工具,pprof 读取 profile.proto 格式的分析样本集合并生成报告以可视化和帮助分析数据,它可以生成文本和图形报告,是用来分析应用程序的性能重要工具,也可以作为其他用途,例如查看反汇编、和Linux perf工具结合使用。


2 安装

golang本身自带pprof工具,pprof可视化时需要依赖Graphviz工具。


linux环境下安装Graphviz:

yum install -y graphviz


windows环境安装Graphviz:

# Graphviz下载地址: https://graphviz.org/download/
# (1) 安装,在界面默认是没有选择加入系统path,选择添加到系统path即可。(注:如果是默认安装,需要手动把`Graphviz/bin`目录放到系统path。)

# (2) 生成配置文件,进去 `Graphviz/bin` 安装目录,执行命令:
dot -c



3 使用模式

3.1 报告生成

pprof 生成指定格式的报告并退出,格式可以是文本或图形。

pprof <format> [options] source


3.2 交互式终端使用

pprof 启动一个交互式 shell,用户可以在其中键入命令,输入help获取命令帮助信息。

pprof [options] source


3.3 网页界面

pprof 可以指定端口上开启 HTTP 服务,在浏览器中访问该端口(例如 http://localhost:7778 ),相对前两种模式,比较常用。

pprof -http=[host]:[port] [options] source



4 一个go程序性能分析示例

下面代码是分配栈对象和堆对象测试代码,可以调整heapGSizestackGSize变量大小来调整栈对象和堆对象数量,随着goroutine数量增大,gc压力增大,GC阶段占CPU时间越来越大,CPU执行用户逻辑占比变小,说明程序性能下降。

package main

import (
    "fmt"
    "math/rand"
    "net/http"
    _ "net/http/pprof"
    "time"
)

var chOut = make(chan string, 100)

func main() {
    // 开启pprof
    go func() {
        err := http.ListenAndServe(":7777", nil)
        if err != nil {
            panic(err)
        }
    }()

    // 20个goroutine创建堆对象
    heapGSize := 20
    for i := 0; i < heapGSize; i++ {
        NO := i
        go func() {
            for {
                heapVar(NO)
                sleep()
            }
        }()
    }

    // 20个goroutine创建栈对象
    stackGSize := 20
    for i := 0; i < stackGSize; i++ {
        NO := i
        go func() {
            for {
                stackVar(NO)
                sleep()
            }
        }()
    }

    go output(chOut)

    <-time.After(time.Hour)
}

func heapVar(NO int) {
    for i := 0; i < 2000; i++ {
        s := make([]int, 10240) // 10K
        s[0] = i
        if i == 0 { // 每次只把2000个堆对象中的第一个堆对象输出查看
            s[0] = time.Now().Second()
            chOut <- fmt.Sprintf("heap%d:%d", NO, s[0])
        }
    }
}

type User struct {
    ID    int
    Name  string
    Age   int
    Email string
}

func stackVar(NO int) {
    for i := 0; i < 100000; i++ {
        number := &User{ID: i}
        if i == 0 {
            number.ID = time.Now().Second()
            chOut <- fmt.Sprintf("stack%d:%d", NO, number.ID)
        }
    }
}

// 随机睡眠1~1.5秒
func sleep() {
    n := rand.Intn(500) + 1000
    time.Sleep(time.Millisecond * time.Duration(n))
}

// 打印输出
func output(ch <-chan string) {
    maxSize := 100
    msgs := make([]string, maxSize, maxSize)
    var v string
    var count int
    for {
        select {
        case v = <-ch:
            count++
            if count == maxSize {
                count = 0
                fmt.Printf(msgs[0] + " ") // 打印第0个位置消息,为了减少终端输出
            }
            msgs[count] = v
        }
    }
}

编译后运行测试程序,在浏览器打开 http://localhost:7777/debug/pprof 可以查看不同类型采集数据。

count  Profile
15     allocs           // 过去所有内存分配的样本
0      block            // 同步阻塞
0      cmdline          // 当前程序的命令行调用
47     goroutine        // 当前所有goroutine的堆栈痕迹
15     heap             // 现场对象的内存分配样本。你可以指定gc GET参数,在进行堆采样之前运行GC
0      mutex            // 争夺锁的持有者
0      profile          // CPU概况。你可以在seconds GET参数中指定持续时间。在你得到概况文件后,使用go工具pprof命令来调查概况
16     threadcreate     // 操作系统创建新线程
0      trace            // 当前程序的执行跟踪。你可以在秒的GET参数中指定持续时间。在你得到跟踪文件后,使用go tool trace命令来调查该跟踪

页面只能查看这些数据简单分析,但可视化不够直观,使用go tool pprof更方便查看对应每个类型profile,在菜单【View】可以查看类型包括:

  • TOP:排名
  • Graph:关系图
  • Flame Graph:火焰图
  • Peek:类似TOP,信息更详细
  • Source:源码


4.1 CPU分析

在实际中,如果发现程序的CPU使用率高时,可以使用go tool pprof采集CPU分析数据。下面命令是采集最近20秒(默认是30秒)CPU执行分析数据,http端口是7778。

go tool pprof -http=:7778 -seconds=20 http://localhost:7777/debug/pprof/profile

等待20秒之后自动在浏览器展示,点击菜单【View】–> 【Flame Graph】查看CPU火焰图,如下图所示:

cpu火焰图

从图中可以看到heapVarstackVar只占用1/3左右的CPU,2/3的CPU时间用来创建对象和回收对象了,说明有优化空间。


4.2 内存分析

在实际中,如果发现程序的内存使用比较高时,可以使用go tool pprof采集内存分析数据,下面命令是查看堆分配情况,http端口是7779。

go tool pprof -http=:7779 http://localhost:7777/debug/pprof/heap

执行命令之后自动在浏览器展示,点击菜单【SAMPLE】 –> 【inuse_space】,然后点击菜单【View】–> 【Flame Graph】查看内存火焰图,如下图所示:

内存火焰图

从图中可以看到heapVar占用比较多,说明需要优化heapVar函数,其他系统底层调用,通常是用户程序调用导致,无法优化。


4.3 下载采集数据到本地分析

除了直接在浏览器分析,还可以把采集的数据下载到本地后再分析,命令如下:

# 下载到本地
wget <url> -O <filename>
# 下载的文件
go tool pprof -http=<:prort> <filename>


下载采集数据到本地之后,可以做优化前后对比,优化前测试程序的heapVarstackVar变量值为20,优化后heapVarstackVar变量值为1.


CPU优化前后对比

# 下载优化前采集数据
wget http://localhost:7777/debug/pprof/profile -O cpu_before.out

# 下载优化后数据
wget http://localhost:7777/debug/pprof/profile -O cpu_after.out

# 比较优化前后
go tool pprof -http=:7778 --base cpu_before.out cpu_after.out

执行命令后,查看Graph图,绿色是优化节省的CPU资源,如下图所示:

优化前后CPU对比


内存优化前后对比

# 下载优化前采集数据
wget http://localhost:7777/debug/pprof/heap -O heap_before.out

# 下载优化后数据
wget http://localhost:7777/debug/pprof/heap -O heap_after.out

# 优化前后比较
go tool pprof -http=:7779 --base heap_before.out heap_after.out

执行命令后,查看Graph图,绿色是优化节省的内存资源,如下图所示:

优化前后内存对比


注:切换到【VIEW】 –> 【Source】源码界面,可以展示哪个函数节省了多少资源。



5 trace分析

通过trace可以看到goroutine数量、线程数量、堆数量、GC、各个goroutine调度情况,可以查看各个线程当前时刻执行哪个goroutine,当前时刻各个goroutine在干什么,使用trace需要两个步骤:

(1) 下载trace数据到trace.out文件,只有1秒内trace数据

wget http://localhost:7777/debug/pprof/trace -O trace.out


(2) 查看trace信息,http端口为7780

go tool trace -http=:7780 trace.out

执行命令之后在浏览界面点击View Trace查看具体跟踪信息,如下图所示:

trace界面图

从图中看出在1秒内,GC占用时间超过30%,说明这个程序是需要优化的。点击界面右边菜单File Size Stats,从统计中可以看到GC相关的事件数量非常多,例如 runtime.bgsweep、SWEEP、GC (dedicated)。


可以选择zoom进行放大看各个goroutine执行细节,在界面右上角搜索事件名称,例如STW、SWEEP等,下面是搜索STW的细节:

STW图

从图中看到在8ms内出现了不少STW(stop the world),在一个GC完整周期出现两次STW,其中绿色是sweep termination阶段的STW,红色是mark termination阶段的STW。


6 总结

通过pprof作为go程序定位问题、性能优化的工具。建议生产环境的go服务开启pprof,正常情况下是不需要实时去采集内存、CPU数据来分析。因为不知道什么时候应该采集数据,建议写一个看门狗程序,检测CPU、内存或其他指标达到阈值时,自动采集保存现场数据,避免出现问题了,没有留下现场数据。



专题「golang相关」的其它文章 »