凌云时刻 · 技术

导读:在开发 golang 程序过程中,go pprof 可以对我们所写的算法甚至整个应用程序做性能分析,帮助我们快速定位效率低或者资源消耗多的地方。Go 提供了 pprof 以及 trace 工具,本文分别介绍这两个工具的简要用法,希望大家能够快速了解以及快速入门 Go 的性能调优。

作者 | 谢久阳(昕希)

来源 | 凌云时刻(微信号:linuxpk)

先来一段演练

在给大家介绍枯燥的基础知识之前,先来看看一个简单的例子,这个例子简单粗暴,可以通过 pprof 快速定位协程阻塞问题。

 示例代码

package main


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


func main() {
    // 是否阻塞的开关
    var flag bool
    testCh := make(chan int)


    go func() {
        // pprof 监听的端口 8080
        if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil {
            fmt.Printf("%v", err)
        }
    }()


    go func() {
        // 永远为 false 的条件
        if flag {
            <-testCh
        }


        // 死循环
        select {}
    }()


    // 每秒执行100次
    tick := time.Tick(time.Second / 100)
    for range tick {
        ch1(testCh)
    }
}


func ch1(ch chan<- int) {
    go func() {
        defer fmt.Println("ch1 stop")
        // 给 ch channel 写入数据
        ch <- 0
    }()
}




 编译成二进制

go build -o goroutine-test main.go

 执行并获取 profile 数据

执行测试二进制文件

./goroutine-test

使用 pprof 工具打开第一次,并退出,再次打开,然后退出 pprof, 退出测试程序

go tool pprof xxx/goroutine-test http://localhost:8080/debug/pprof/goroutine
$home/pprofPPROF_TMPDIR
-base
go tool pprof -base xxx/goroutine-test xxxx001.pb.gz xxxx002.pb.gz

并使用 top 命令进行简单查看

Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 640, 99.53% of 643 total
Dropped 8 nodes (cum <= 3)
      flat  flat%   sum%        cum   cum%
       640 99.53% 99.53%        640 99.53%  runtime.gopark
         0     0% 99.53%        641 99.69%  main.ch1.func1
         0     0% 99.53%        641 99.69%  runtime.chansend
         0     0% 99.53%        641 99.69%  runtime.chansend1
         0     0% 99.53%        641 99.69%  runtime.goparkunlock
(pprof)

从上面内容中,可以看到第二次获取的 profile 文件记录的 goroutine 数量比第一次增加了 640 (flat)个,并且 flat% 占用比例 99.53% ,那基本上就是这里有协程阻塞了

使用 traces 来查看所有 profile 统计信息,包括堆栈的一些内容

(pprof) traces
Type: goroutine
Time: May 6, 2020 at 9:53am (CST)
-----------+-------------------------------------------------------
       641   runtime.gopark
             runtime.goparkunlock
             runtime.chansend
             runtime.chansend1
             main.ch1.func1
-----------+-------------------------------------------------------
         1   net/http.(*connReader).backgroundRead
-----------+-------------------------------------------------------
        -1   runtime.gopark
             runtime.netpollblock
             internal/poll.runtime_pollWait
             internal/poll.(*pollDesc).wait
             internal/poll.(*pollDesc).waitRead
             internal/poll.(*FD).Read
             net.(*netFD).Read
             net.(*conn).Read
             net/http.(*connReader).backgroundRead
-----------+-------------------------------------------------------
641 runtime.goparkmain.ch1.func1
main.ch1.func1
(pprof) list main.ch1.func1
Total: 643
ROUTINE ======================== main.ch1.func1 in /Users/xinxi/Desktop/test/main-goroutine.go
         0        641 (flat, cum) 99.69% of Total
         .          .     32:}
         .          .     33:
         .          .     34:func ch1(ch chan<- int) {
         .          .     35:   go func() {
         .          .     36:       defer fmt.Println("ch1 stop")
         .        641     37:       ch <- 0
         .          .     38:   }()
         .          .     39:}

在上面的代码片段,很快的就会定位到 37 行代码给 ch 写入数字 0 的过程中,造成了大量阻塞,目前增加到了 641 个 goroutine 的阻塞

ch

pprof 提供了三种方式的使用

 Benchmark

基于基准测试的 pprof,对于已经写好的算法包来说,可以利用基准测试和 pprof 来校验算法是否高效、内存消耗是否合理。

-cpuprofile-memprofile
# 获取cpu的profiling文件
go test -bench=. -benchmem -cpuprofile profile.out
# 获取heap的profiling文件
go test -bench=. -benchmem -memprofile memprofile.out
# 获取cpu以及heap的profiling文件
go test -bench=. -benchmem -memprofile memprofile.out -cpuprofile profile.out
go tool pprof

 单独程序调用

需要引入的包:

import _ "runtime/pprof"

cpu 数据记录:

// file 是 os.File 结构,用来创建 pprof 要写入的文件
// 开始写入 cpu 的分析信息
pprof.StartCPUProfile(file)


// 必须要添加这个,将所有信息在结束前都写入到文件中
defer pprof.StopCPUProfile()

heap数据记录:

// file 是 os.File 结构,用来创建 pprof 要写入的文件
// 回收之前数据,用来获取最新统计数据
runtime.GC()


// 开始写入堆数据
pprof.WriteHeapProfile(file)
go tool pprof

 web server

如果我们要 profiling 一个服务,比如说 http 或者 grpc 的服务端,那么使用 web server 的方式会更合适。会对外开辟一个 web server,通过指定的端口来访问和下载 pprof 数据

需要引入的包:

_ "net/http/pprof"

需要 main 函数中添加如下内容:

func main() {
    go func() {
        if err := http.ListenAndServe("0.0.0.0:8080", nil); err != nil {
            fmt.Printf("%v", err)
        }
    }()


    ...
}

这种方式可以查询以下几种信息:

runtime.SetBlockProfileRateruntime.SetMutexProfileFraction

pprof 环境变量以及常用参数

 环境变量

PPROF_TMPDIR:存储pprof文件的位置,默认位置 $HOME/pprof

PPROF_TOOLS:binutils 工具路径,默认在当前目录查找;pprof 工具使用 GNU Binutils 来校验以及反编译二进制;感兴趣的同学可以查阅:https://www.gnu.org/software/binutils/

$HOME/pprof/binaries$name$path$buildid\$name$path\$buildid

 常用参数

  • -base:提供两个 profile 文件,以第一个 profile 为基准,用第二个减去第一个统计的结果(这个参数会更常用一些)

  • -diff_base:提供两个 profile 文件比较,显示的百分比是基于第一个 profile 统计的数量

  • -http:以 Web UI 的方式打开 pprof 文件

  • -seconds:设置指定秒为间隔获取 pprof 信息,这个只在获取时间统计的类型中才生效,如 cpu profiles

  • -cum:以累计总量排序

  • -flat:以当前统计的数量排序

  • -inuse_space:展示使用的空间

  • -inuse_objects:展示使用的对象

  • -alloc_space:展示总共分配的空间

  • -alloc_objects:展示总共分配的对象

pprof 操作

 常用字段含义

  • flat:当前函数占用 profile 样本的数量

  • flat%:当前函数占用 profile 样本的百分比

  • sum%:当前行以上所有 flat% 的和

  • cum:累计的 profile 样本量(cum全写:cumulative)

  • cum%:累计 profile 样本量占总量的百分比

对于上面的字段,不同的 profile 含义基本相似,比如说 heap profile, flat 就是当前函数内存占用量,如果是 goroutine profile,则是当前函数 goroutine 数量等。

如下面的例子,展示内存占用量:

Showing nodes accounting for 1056.67kB, 100% of 1056.67kB total
Showing top 10 nodes out of 30
      flat  flat%   sum%        cum   cum%
  544.67kB 34.72% 34.72%   544.67kB 34.72%  google.golang.org/grpc/internal/transport.newBufWriter
  512.01kB 32.64%   100%   512.01kB 32.64%  github.com/golang/protobuf/proto.makeMessageMarshaler
         0     0%   100%   512.01kB 32.64%  github.com/golang/protobuf/proto.(*InternalMessageInfo).Size
         0     0%   100%   512.01kB 32.64%  github.com/golang/protobuf/proto.(*marshalFieldInfo).computeMarshalFieldInfo
         0     0%   100%   512.01kB 32.64%  github.com/golang/protobuf/proto.(*marshalFieldInfo).setMarshaler

 控制台

常用命令

  • top:默认展示前 10 条样本计数最高的,后面接数字,则显示指定数字的条目,如:top3

  • traces:输出所有 profile 的统计信息

  • list:输出给定正则表达式匹配的方法源码,list 后面是可以接正则表达式

  • tree:输出所有调用关系

  • web:生成 profile 的 svg 矢量图片并用 web 打开,如果不指定参数则显示所有,给定指定方法,则显示指定的方法

  • pdf:生成 pdf 的 profile 文件,里面展示 profile 图片的内容

  • cum:按照累计的 profile 样本量排序

  • flat:按照当前函数占用的 profile 排序

正如我们最刚开始的示例一样,top、traces 和 list 是最常用的三个命令,通过 top 来快速查看那里占用最高,然后通过 traces 来快速定位,最后通过 list 来查看哪里代码的问题

 Web

在使用 pprof 命令的时候指定 -http 参数即可,如:

go tool pprof -http=:8080 [profiling file path]

 图形部分内容含义

  • Flat 值:当前函数 profile 的值,在图表中以字体大小来展示,字体大,代表 flat 值大;字体小,代表 flat 值小

  • Cum 值:累计的 profile 值(包含子节点),在图表中以节点的颜色来展示,红色代表当前值比较大,灰色代表值比较小,绿色代表值为负的

  • 实体线:当前节点调用另外一个节点,箭头指向谁,则代表当前节点调用谁

  • 粗的红色实体线:在这个路径上使用了更多的资源

  • 细的实体线:在这个路径上使用了相对较少的资源

  • 虚线:代表省略了当中的过程,pprof 会根据情形来省略其中一些内部调用关系,让整套图展示更容易

http://host:port/ui/flamegraph

看了这么多基础的环境变量、参数以及命令,那么再回过去回味一下刚开始的示例,结合命令行以及web图形再操作一遍,就会更轻松的理解,另外需要根据自己碰到不同情况来使用不同的命令和参数。

Trace

Trace 也是 go 工具链中比较重要的工具,它与上面 pprof 的本质区别为:

  • pprof 更擅长去查看程序效率,比如说你要查看谁占用内存时间长,谁的协程阻塞了等等

  • trace 则是程序在运行期间的信息追踪,通过可视化的方式来查看这期间程序到底做了什么,以及了解 GC 对程序的影响等等

但这两套工具配合起来,就会让我们更快速的发现问题,以及解决问题

-trace
// 需要引入的包
import _ "runtime/pprof"


// file 是 os.File 结构,用来创建 pprof 要写入的文件
// 开始 trace
trace.Start(file)
// 结束 trace,并将trace信息完全写入到文件中
defer trace.Stop()

执行完毕后,会生成 trace 的文件。

对于 web 来说,通过下面命令来下载指定时段的 trace 文件:

curl "http://localhost:8080/debug/pprof/trace?seconds=50" > trace.log

其中 ?seconds=50 是指获取50秒的 trace 信息,可根据具体情况来设置这个时间

 查看 trace 信息

通过如下命令来打开 trace:

go tool trace [trace file path]

执行完上面的命令后,会自动打开浏览器,并且在默认页面上会显示几个超链接

  • View trace:以图形界面的形式展示 trace 的内容

  • Goroutine analysis:协程分析,会列出协程数量以及各个协程调用时间

  • Network blocking profile:用调用关系图展示网络阻塞的情况

  • Synchronization blocking profile:同步阻塞耗时情况,使用调用关系图来展示

  • Syscall blocking profile:系统调用阻塞耗时情况,使用调用关系图来展示

  • Scheduler latency profile:调度器延迟耗时情况,使用调用关系图来展示

  • User-defined tasks:用户自定义 trace 的 tasks

  • User-defined regions:用户自定义 trace 的 region

  • Minimum mutator utilization:不太好翻译成中文(保持原有名词,大家自己体会),这个会用一个曲线图来展示 GC 对我们程序影响的情况,它可以用来分析和判断是否要对 GC 做优化

User-defined tasksUser-defined regionsmessagetaskregionregiontaskmessagetaskregion
View traceMinimum mutator utilization

 View trace

点开 View trace 后,会显示下面的图形

通过 trace 图形,可以快速看到程序整体运行耗时的时间,以及 GC 等信息,也可以看到所有执行中的协程。

在这个界面中,可以通过快捷键来操作:

wsad

界面中方的图形是矢量的,我们可以放大来查看具体的协程执行情况,以及 GC 的 mark assist 和 STW(Stop the world)情况

上面图太复杂了,我们找一个协程数量少的图形来给大家看一下

在这个图中,我们有6个协程,在每个协程上都有其编号,他们的含义如下:

  • G1:main 函数

  • G18:trace 的协程

  • G19,G20,G21,G22 是 main 函数中开启的四个协程

第一个 G1 到最后那个 G1 结束,就是程序运行的时间;从图中可以快速发现 G19 到 G22 协程中间都有一块空白,这空白就是协程暂停了(可能是在等待什么,导致阻塞了),四个协程都暂停了一小段时间才继续执行的,会导致效率变低,可能无法达到我们的预期。

对于这个暂停,是我添加了一个互斥锁,这个锁要等一小段时间才会解锁,所以每个协程在获取 task 的时候都需要等待锁解锁才能继续执行,因此造成了性能不符合预期的情况。那么对于这个锁,我们可以根据自己的需要来做各种方式的优化,以达成最优效果。

 Minimum mutator utilization

这个工具是在 go 1.12 这个版本中引入进来的,他的主要作用是辅助查看 GC 对我们程序的影响,默认的图形如下:

从这个曲线图中,可以看到,大约80微秒后我们的程序才获得到 cpu 使用,那在这 80 微秒之前,GC 做了 STW (stop the world),随着横坐标的时间增加,曲线开始逐渐趋近 100%,意味着我们的程序对 cpu 可用率越来越高,从这个曲线上来看,GC 还可以,并不需要做什么优化,如果曲线随着时间增加依然不高,说明 GC 负担比较重,占用了更多系统资源,这个时候我们需要对 GC 做优化了。

曲线图右侧有几个选项,他们的含义是:

  • System:整个系统的利用率

  • Per-goroutine:每一个协程的利用率

  • STW:Stop the word,表示所有协程同时暂停,在做 GC 的时,是会 STW

  • Backgroud workers:GC 的协程,会消耗 25% 的利用率,在 GC 的时候,会以后台方式执行

  • Mark assisst:GC 要减慢内存分配速度时,会分配 goroutine 来做标记

  • Sweep:回收在 GC 之间未使用的内存

  • Show percentiles:除了最基本数据外,还显示 mutator 利用率的百分比

每选择一个选项,曲线都会有相应的变化,大家可根据各自需求来查看。

总结

对于开发人员来说,单元测试,code review 甚至集成测试都是非常重要的,但这些方法未必能够发现和定位隐性问题,如:goroutine 阻塞,内存消耗过多,cpu 利用率超高等等,这些问题在前期是很难发现的,但如果不能在上线前发现和解决,则会容易碰到一些始料未及的故障,会非常被动,所以花一些时间对程序做一轮性能分析和调优,也许会有意想不到的效果。

END

往期精彩文章回顾

长按扫描二维码关注凌云时刻

每日收获前沿技术与科技洞见