摘要:
当我们的应用程序出现cpu使用率过高,内存泄漏等一系列性能问题时,就要采集程序运行时的数据,基于采集到的数据借助工具进行性能分析,优化程序性能。对于Go程序而言,我们就可以借助golang 官方提供的pprof工具对程序进行性能分析。本篇文章就详细的介绍下,如何使用pprof这个Go性能分析的大杀器,文章有点长但干货满满!
pprof简介
pprof通常(还有其他的)用于分析以下几类性能数据:
| 类别 | 用途 |
|---|---|
| CPU Profiling | 按照一定的频率分析程序运行时CPU的使用情况,帮助我们优化程序耗时 |
| Memory Profiling | 监视应用程序当前和历史堆内存的使用情况,以检查内存泄漏。注意仅监视堆内存,对于栈空间函数调用结束后会自动回收不用消耗性能进行GC,所以我们也不用进行分析 |
| Mutex Profiling | 监视互斥锁的争用情况,供我们优化Goroutine之间互斥等待的时间,减少data race |
| Goroutine Profiling | 分析当前应用程序的Goroutine情况,可以对当前正在运行的 Goroutine 进行堆栈跟踪和分析。这对我们分析Goroutine的阻塞,回收状态,排查Goroutine是否出现泄漏非常有用。Goroutine Profiling和Memory Profiling也密切相关,因为Goroutine一旦泄漏往往也会造成内存泄漏 |
另外pprof包有两个,分别是 runtime/pprof 和 net/http/pprof,其中runtime/pprof用于分析非web型server,net/http/pprof用于分析web server,每个包都可以用于以上几类性能分析,并且每种分析方式大的流程步骤都是一样的:首先采集程序的运行数据,然后分析运行数据。分析运行数据时pprof工具也提供了几种分析方式,一种是基于交互式终端,通过命令行的方式进行分析,第二种就是基于web 可视化界面进行分析,这种方式更方便,更直观,线上分析中更推荐用这种。下面都会一一介绍,先来看一下runtime/pprof包的使用
runtime/pprof
1 CPU Profiling
1.1 程序demo
package main
import (
"flag"
"fmt"
"log"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"sync"
"syscall"
"time"
)
var cpuProfiling = flag.String("cpuprofile", "", "write cpu profile to `file`")
func main() {
flag.Parse()
if *cpuProfiling != "" {
f, err := os.Create(*cpuProfiling)
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()
}
clo := make(chan struct{})
go func() {
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
for range sig {
clo <- struct{}{}
}
}()
for {
select {
case <-clo :
return
default:
time.Sleep(time.Millisecond*time.Duration(10))
currencyPattern()
}
}
}
//扇入模型
func currencyPattern() {
fanin := func(chs ...<-chan interface{}) <-chan interface{} {
ch := make(chan interface{})
var g sync.WaitGroup
for _, c := range chs {
g.Add(1)
go func(c <-chan interface{}) {
defer g.Done()
for value := range c {
ch <- value
}
}(c)
}
go func() {
g.Wait()
close(ch)
}()
return ch
}
create := func() []<-chan interface{} {
ret := make([]<-chan interface{}, 0)
for i:=0; i<100; i++ {
func(num int) {
ch := make(chan interface{})
go func() {
defer close(ch)
time.Sleep(time.Millisecond*time.Duration(num))
ch <- fmt.Sprintf("f%d", num)
}()
ret = append(ret, ch)
}(i)
}
return ret
}
total := fanin(create()...)
for v := range total {
fmt.Println(v)
}
}
1.2 采集数据
编译并运行以上代码
go build -o pprof
./pprof --cpuprofile=cpu.pprof
在当前目录下,会生成cpu.pprof文件,这个文件是ProtoBuf的数据格式,下面先对cpu.pprof文件进行CPU Profiling
1.3 分析数据
1.3.1 命令行交互式终端分析
go get github.com/google/pprof(安装pprof工具)
go tool pprof democpu.pprof
pprof涉及很多交互式命令,输入help可查看每个命令的详解,输入top默认输出耗时排前10的函数。可以看到输入top命令后总共输出有六列,其中最后一列是函数名称,flat 表示调用一次当前函数消耗的时间,flat% 则表示调用一次当前函数占总样本时间(410ms)的比例,sum% 则表示在总的410ms的样本时间中当前函数占比多少,以currencyPattern为例可以看出该函数总共被调用了31次(75.61/2.44向上取整),cum表示调用完当前函数总共累计消耗的时间,cum% 则表示累计占比。还是以currencyPattern为例,可以看出currencyPattern之后调用的函数总共消耗了60.98%的cpu时间
输入list加具体的函数名,可以看到函数内每行代码消耗的时间,以currencyPattern为例:在currencyPattern函数内fanin是func1,create是func2。其中fmt.Println(v)这一行io输出消耗了100ms,调用fanin和create总共消耗了40ms,而调用fanin则消耗了10ms,调用create消耗了30ms。其他的以此类推就不一一详述了。可以看到就能快速的分析出我们程序的耗时,进而对不合理的耗时进行优化。提高程序性能
1.3.2 图形可视化分析
这种分析方式,通过图形的方式把采集到的数据直观的展示出来,相比于命令行交互式分析,免去了我们熟悉每个命令的怎么用的麻烦。图形可视化分析的方法又分为两种,一种是通过安装 graphviz 软件,然后在交互式终端输入web命令生成svg图,然后用浏览器打开svg文件。第二种就是通过pprof启动一个指定端口的程序然后我们动态的访问这个页面直接浏览。这里我就仅展示第二种方式
运行命令:go tool pprof -http=:8887 cpu.pprof (端口任意指定,只要是空闲端口即可)
然后浏览器访问 http://localhost:8887/ui/ ,即可在浏览器上分析性能数据。VIEW菜单下,可以切换数据的展示模式,如图表Graph,火焰图Flame Graph等。下图就是以Graph的方式展示的cpu profiling数据(文件太大,只截取了一小部分图)。图中从上往下展示的就是完整的调用链路
关于图形Graph:
- 每个框代表一个函数,框越大表示占用的 cpu 资源越多
- 每个框之间的线条代表函数之间的调用关系,线条上的数字表示下层函数的耗时
- 每个框中第一个数字表示当前函数占用 cpu 的百分比,第二行数字表示当前函数累计占用 cpu 的百分比
关于火焰图Flame Graph:
火焰图是性能分析专家Bredan Gregg 创建的一种性能分析图,之前go程序要生成火焰图比较麻烦,要安装go-torch,FlameGraph等工具,还要搭建perl环境。现在pprof已经把火焰图整合进来了,我们在上面的UI界面中直接切换到Flame Graph即可。
火焰图展示的也是从上到下的完整调用链,每行的每个方块代表一个函数,它的下一层方块表示当前函数会调用哪些函数,每个方块的大小代表了CPU的耗时长短。鼠标hover到对应的方块上可以看到cum以及cum%的数据。点击对应的方块可以以当前方块为父节点查看其下更详细的调用链。如点击main.currencyPattern,可以看到内部主要的耗时消耗在哪,每部分的占比等
1.4 All in all
总而言之不管是采用命令行交互式的分析方式,还是图形化分析的Graph和Flame Graph方式,都可以达到分析性能指标的目的。哪种方式更直观,哪种方式更简便我们就选哪种
2 Memory Profiling
在开始正式动手内存分析之前,先来看一下Dave《High Performance Go Workshop》 中关于Memory Profiling能做什么不能做什么的描述
简而言之Memory Profiling记录的是堆内存的分配情况,不记录栈内存,并且Memory Profiling是基于抽样的,默认是每1000次堆内存分配,抽样一次。并且Memory profiling跟踪的是已分配的但未使用的内存,而不是使用中的内存,所以Memory profiling并不能衡量程序整体的内存使用情况。Dave个人也认为,使用Memory Profiling不能够发现内存泄漏的情况。其实单个函数或单个程序的耗时分析是最简单的。而Mem Profiling或Goroutine Profiling则是相对比较困难的,因为一旦我们需要对程序进行Mem Profiling或Goroutine Profiling,往往就意味着我们的程序在线上出现了内存泄漏,这时就需要结合Mem Profiling和Goroutine Profiling去分析问题,看看到底是我们的程序内存分配不合理还是Gouroutine泄漏,或者是引入的第三方包有问题等等,导致的内存泄漏。
2.1 程序demo
我们把上面CPU Profiling的程序稍微改造一下,如下
package main
import (
"flag"
"log"
"os"
"os/signal"
"runtime"
"runtime/pprof"
"strconv"
"sync"
"syscall"
)
var memProfiling = flag.String("memprofile", "", "write mem profile to `file`")
var buf []byte
func main() {
flag.Parse()
clo := make(chan struct{})
var g sync.WaitGroup
g.Add(2)
go func() {
defer g.Done()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
for range sig {
clo <- struct{}{}
}
}()
go func() {
defer g.Done()
for {
select {
case <-clo :
return
default:
currencyPattern()
}
}
}()
if *memProfiling != "" {
f, err := os.Create(*memProfiling)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer f.Close()
runtime.GC()
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal("cound not write memory profile: ", err)
}
}
g.Wait()
}
//扇入模型
func currencyPattern() {
fanin := func(chs ...<-chan interface{}) <-chan interface{} {
ch := make(chan interface{})
var g sync.WaitGroup
for _, c := range chs {
g.Add(1)
go func(c <-chan interface{}) {
defer g.Done()
for value := range c {
ch <-value
}
sli := []string{"1","2","3","4","5","6","7","8","9","10","11","12","13"}
for _, s := range sli {
strconv.Atoi(s)
}
}(c)
}
go func() {
g.Wait()
close(ch)
}()
return ch
}
create := func() []<-chan interface{} {
ret := make([]<-chan interface{}, 0)
for i:=0; i<100; i++ {
func(num int) {
ch := make(chan interface{})
go func() {
defer close(ch)
ch <- num
}()
ret = append(ret, ch)
}(i)
}
return ret
}
total := fanin(create()...)
for v := range total {
size, ok := v.(int)
if ok {
buf = append(buf, make([]byte, 1024*size)...)
}
}
}
2.2 采集数据
同样的我们编译并运行以上代码
go build -o pprof
./pprof --memprofile=mem.pprof
在当前目录下会生成mem.pprof文件,下面先对mem.pprof文件进行Memory Profiling
2.3 分析数据
2.3.1 命令行交互式终端分析
go tool pprof mem.pprof
输入top指令可以看到main.currencyPattern函数消耗的堆内存最多(flat sum cum等含义和上面的CPU Profiling的含义类似,以此类推即可),再输入list main.currencyPattern可查看函数内具体哪行代码消耗了大量内存,traces指令查看函数调用栈,可以发现每次调用之后堆内存都是呈递增的趋势。所以我们明显可以优化一下
buf = append(buf, make([]byte, 1024*size)...) 这行代码
2.3.2 图形可视化分析
运行命令:go tool pprof -http=:8889 mem.pprof (端口任意指定,只要是空闲端口即可)
然后访问 http://localhost:8889/ui/ 就可以查看Graph以及Flame Graph等更清晰的图表,然后基于这些图表进行分析。
2.4 All in all
写过C/C++的小伙伴相信对内存分配都有着一定的敬畏之心,虽然golang有自身的GC机制,不像C/C++一样需要我们手动释放内存。但内存分配不合理,导致程序反复大量的触发GC,也会影响程序本身的性能。在日常开发中熟知哪些情况下会导致内存逃逸(因为内存一旦逃逸就需要GC进行回收),我们甚至可以不用进行Memory Profiling,也可以知道哪些内存操作是不合理的了
net/http/pprof
下面再分享一下如何基于net/http/pprof包对web程序进行性能调优。当我们使用标准库中的net/http包构建http server时,我们只用简单引入net/http/pprof即可采集server的性能数据进行分析,如下:
import _ "net/http/pprof"
但在实际业务中,我们构建的http服务基本都是比较复杂,并且要具备较高的性能。所以实际业务中我们往往会使用优秀的web框架,如gin,fasthttp,echo,iris等等。那么使用这些web框架时,我们又该怎样引用net/http/pprof包,来采集线上服务的性能数据呢。我们都知道 以import _ "net/http/pprof" 的方式引包,其实就是执行了包下的init函数,而net/http/pprof的init函数如下:
可以看到,它其实就是拿标准库中的DefaultServeMux,注册了一些HandleFunc,那对于web框架我手动注册一下即可,下面以gin 框架构建的web server为例,进行Goroutine Profiling。CPU Profiling和Memory Profiling跟runtime/pprof包的类似,就不再分享了。
1 Goroutine Profiling
1.1 程序demo
这个程序每次请求后,都会有大量Goroutine未正常退出,最终会导致内存泄漏
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"net/http/pprof"
"strconv"
)
var (
AppRouter *gin.Engine
)
func InitRouter() {
AppRouter = gin.Default()
AppRouter.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "ok")
})
}
func pprofHandler(h http.HandlerFunc) gin.HandlerFunc {
handler := http.HandlerFunc(h)
return func(c *gin.Context) {
handler.ServeHTTP(c.Writer, c.Request)
}
}
func main() {
InitRouter()
pprofgroup := AppRouter.Group("/debug/pprof")
pprofgroup.GET("/", pprofHandler(pprof.Index))
pprofgroup.GET("/cmdline", pprofHandler(pprof.Cmdline))
pprofgroup.GET("/profile", pprofHandler(pprof.Profile))
pprofgroup.GET("/symbol", pprofHandler(pprof.Symbol))
pprofgroup.GET("/trace", pprofHandler(pprof.Trace))
pprofgroup.GET("/goroutine", pprofHandler(pprof.Handler("goroutine").ServeHTTP))
AppRouter.GET("/pprof/goroutine/profiling", dealFunc)
if err := AppRouter.Run(":8888"); err!= nil {
panic(err)
}
}
func dealFunc(ctx *gin.Context){
log.Println("dealFunc request begin")
defer log.Println("dealFunc request end")
count := ctx.Query("count")
tmp,_ := strconv.Atoi(count)
for i:=0; i<tmp; i++{
go func() {
sli := make([]int, 0)
for j:=0; j<1000; j++ {
sli = append(sli, j)
}
select {}
}()
fmt.Println(i)
}
ctx.JSON(http.StatusOK,"ok ok")
}
1.2 采集数据
同理我们先编译并运行以上代码:
go build -o pprof(编译)
./pprof(运行)
相比于runtime/pprof包的使用稍有不同的是,我们需要用pprof工具持续监听我们程序的profile接口,如下监测60秒,然后开启一个新窗口,并用hey工具持续的进行压测(以免采集到空数据):
go tool pprof --seconds 60 http://localhost:8888/debug/pprof/goroutine (监测)
go get -u github.com/rakyll/hey (安装hey)
hey -n 1000 http://localhost:8888/pprof/goroutine/profiling?count=100 (压测)
1.3 分析数据
1.3.1 命令行交互式终端
输入top指令可以看到runtime.gopark挂起的goroutine有300000个(因为我实际压测了三回),明显出现了gouroutine泄漏,而导致runtime.gopark的是上一层main.dealFunc.func1函数,可以再执行list 看一下这个函数内部具体哪行代码导致,同理traces命令也可以看出来
1.3.2 图形可视化分析
执行命令:
go tool pprof -http="localhost:8889" pprof.pprof.goroutine.002.pb.gz
再访问 http://localhost:8889/ui/ ,即可查看Graph或Flame Graph,如下图:
1.4 All in all
通过这个例子,可以看到net/http/pprof相比于runtime/pprof,只是在数据采集那一步稍有不同,数据分析的方式其实是一样的。还有就是net/http/pprof对于不同的web框架稍有不同,同理的对不同的rpc框架runtime/pporf包的引用也会稍有不同。所以大家务必灵活应用
总结
关于golang性能分析大杀器pprof的详细运用,就分享到此了。因为pprof较为强大,而且又分runtime/pprof和net/http/pprof两个包,两个包的使用又稍有不同,并且分别运用于不同的server框架时又有些许不同,所以没办法完全列举,但是相信如果你详细看完了本篇文章,并且理解透彻了每个点,对于pprof你就已经掌握的差不多了。关于Go程序性能分析,pprof属于一个相对宏观的工具,通过pprof能够知道哪个函数耗费了多少CPU,分配了多少内存等。但如果想要知道程序中更加细节的指标,比如每个goroutine执行了多长时间,gc的时间线,程序cpu使用率的时间线等等,pprof都是无能为力的。这些指标的分析,就要依赖Go后花园的另一个分析工具trace了,关于trace就请期待下一篇分享吧。