如何获取运行的go程序的profile信息

marlonche · 2018-01-31 12:20:36 · 11109 次点击 · 预计阅读时间 11 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2018-01-31 12:20:36 的文章,其中的信息可能已经有所发展或是发生改变。

go程序从开始运行起,runtime就按照一定频率对内存分配进行采样记录:当内存分配每达到一定值(默认是512KB,由runtime.MemProfileRate设定), runtime就会记录下当前这次内存分配的大小,call stack等信息到profile里面。

而CPU的profile是从调用runtime/pprof包的pprof.StartCPUProfile()开始,到pprof.StopCPUProfile()结束,每1秒采样100次,每次采样时记录下当前正在执行的所有goroutine的call stack,某个函数在这些快照里面出现的次数越多就说明这个函数越耗时。

运行的go程序的所有profile信息都是通过在运行时调用runtime/pprofruntime/trace两个包的接口获取,调用这些接口的方式有直接调用和通过http请求间接调用两种,下面我们说明各种常用的profile信息以及它们的获取,使用方式。

1. 当前所有goroutine的函数调用栈

用处:

查看当前有哪些goroutine在运行,数量对不对,有没有出现很多goroutine卡在同一个地方的情况,有没有死锁的情况,间隔的刷新页面看看各个goroutine数量变化情况,了解程序运行状况变化。

通过http请求间接获取:

在程序中引入包net/http/pprof并只使用它的初始化:

import _ "net/http/pprof"

这一句导致源码里面实际执行的是:

func init() {
    http.HandleFunc("/debug/pprof/", Index)
    http.HandleFunc("/debug/pprof/cmdline", Cmdline)
    http.HandleFunc("/debug/pprof/profile", Profile)
    http.HandleFunc("/debug/pprof/symbol", Symbol)
    http.HandleFunc("/debug/pprof/trace", Trace)
}

上面的url和处理函数都被注册到了http包的DefaultServeMux里面,所以要想使用这些url就必须使用DefaultServeMux来监听某一端口:

http.ListenAndServe(":9999", nil)

这里第一个参数可以是本机的任一端口,当第二个参数为nil时就使用http包的DefaultServeMux来监听并处理http请求, 或者当程序里面已经有自定义的ServeMux时,可以像上面的init函数那样把pprof的处理函数注册到自定义的ServeMux

在程序里面添加上面的代码后,运行时我们可以通过浏览器或wget访问下面的http地址查看goroutine信息:

http://localhost:9999/debug/pprof/goroutine?debug=1

或者进入index页面查看更多项:

http://localhost:9999/debug/pprof

直接获取:

不常用,可以查看源码

2. 一段时间内的CPU profile

用处:

查看消耗CPU的热点函数,能否减少调用次数,能否避免重复计算。

通过http请求间接获取:

同第1点里面获取goroutine信息一样,在程序中引入包net/http/pprof并只使用它的初始化:

import _ "net/http/pprof"

然后使用DefaultServeMux来监听某一端口:

http.ListenAndServe(":9999", nil)

在程序里面添加上面的代码后,运行时我们可以通过浏览器或wget访问下面的http地址获取CPU profile:

http://localhost:9999/debug/pprof/profile?seconds=30

这个http请求会等seconds参数指定的秒数,这期间读取请求返回的二进制格式内容,我们把返回的内容保存为文件,假设取名为cpu.prof, 这个文件里面包含了程序收到请求后的seconds秒内运行占用CPU的情况。

接下来我们通过在命令行输入go tool pprof来查看cpu.prof:

go tool pprof cpu.prof

这个命令会进入命令行交互,我们可以输入很多子命令来查看相关信息,最常用的几个子命令是:

top N

该子命令会列出最耗时的N个函数调用,在输入top命令前输入cum或flat可以使得top列出的列表按cum或flat列排序,flat是单个函数自身(不计函数里面调用的其它函数)的耗时,cum是函数从进入到退出总的耗时。

如果要看某个函数具体是在什么地方耗时,可以使用list子命令查看:

list func_name_in_top_list

该命令会显示耗时的代码和行号, 如果提示找不到source信息,可以在go tool pprof后面指定保存在本地的被profile程序的binary文件,并且这个binary文件要和生成profile文件的程序是在同一平台下编译的。比如在本地的Windows机器上远程profile linux机器上运行的go程序时,确保本地指定的binary是linux机器上编译的:

go tool pprof mybinarypath prof_file_from_remote_host
go tool pprof mybinarypath https://myremotehost/debug/pprof/profile

为了更直观的看出热点函数的调用关系,我们更多的是使用svg和web子命令:

svg > xxx.svg
web

svg命令会生成一个指定文件名的svg文件,web命令会生成一个svg文件并用浏览器打开这个文件,执行web命令前先确保安装了graphviz(brew install graphviz, 然后brew list graphviz查看安装位置并将安装位置的bin目录加到PATH),并确保svg文件的默认打开程序是浏览器。 通过web子命令在浏览器打开的这个svg文件非常有用, 可以清楚的看到耗时函数的调用关系图。

上面是通过浏览器或wget获取cpu.prof文件,然后使用go tool pprof命令来分析这个文件,更直接的方式是直接在命令行输入:

go tool pprof http://localhost:9999/debug/pprof/profile?seconds=30

这个命令会等seconds指定的秒数,这期间读取请求返回的二进制格式内容并写入临时文件,再用go tool pprof查看这个文件并进入命令行交互,过程和上面保存到自定义文件是一样的。

直接获取:

上面通过http请求间接获取的方式实际上最终还是url的handler调用了runtime/pprof包的接口

import "runtime/pprof"

func Profile(w http.ResponseWriter, r *http.Request) {
    sec, _ := strconv.ParseInt(r.FormValue("seconds"), 10, 64)
    if sec == 0 {
        sec = 30
    }

    if durationExceedsWriteTimeout(r, float64(sec)) {
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.Header().Set("X-Go-Pprof", "1")
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout")
        return
    }

    // Set Content Type assuming StartCPUProfile will work,
    // because if it does it starts writing.
    w.Header().Set("Content-Type", "application/octet-stream")
    if err := pprof.StartCPUProfile(w); err != nil {
        // StartCPUProfile failed, so no writes yet.
        // Can change header back to text content
        // and send error code.
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.Header().Set("X-Go-Pprof", "1")
        w.WriteHeader(http.StatusInternalServerError)
        fmt.Fprintf(w, "Could not enable CPU profiling: %s\n", err)
        return
    }
    sleep(w, time.Duration(sec)*time.Second)
    pprof.StopCPUProfile()
}

在查看线上的服务在不同场景下,不同时间段内的运行情况时,使用上面的方式还是很方便的; 但在开发或者优化的时候,我们会改动一些代码,然后想和改动前对比下,看看性能是否有提升,这时我们就需要在代码中确定固定的开始和结束位置来profie,比如某个函数从进入到退出,或某个测试程序从启动到退出,就不能像上面那样大致在某个时间点发出http请求,然后大致等一段时间;这时我们需要在程序里面直接调用runtime/pprof包的接口:

import "runtime/pprof"

var cpuprofile = flag.String("cpuprofile", "", "write cpu profile `file`")

func main() {
    flag.Parse()
    if *cpuprofile != "" {
        f, err := os.Create(*cpuprofile)
        if err != nil {
            log.Fatal("could not create CPU profile: ", err)
        }
        if err := pprof.StartCPUProfile(f); err != nil {
            log.Fatal("could not start CPU profile: ", err)
        }
        defer pprof.StopCPUProfile()
    }

    // ... rest of the program ...
 }

上面的代码cup profile的周期是从程序启动到退出,profile的内容保存到cpuprofile参数指定的文件,和前面通过http请求获取的cpu.prof一样,我们通过go tool pprof来查看这个文件。

除了像上面那样在程序中加profile的代码,go test命令自带profile功能,可以在运行go test命令时指定profile相关的flag,例如下面的命令运行当前目录下的测试用例并将cpu和heap profile写到指定的文件,然后我们就可以用go tool pprof来查看这些profile文件了:

go test -cpuprofile cpu.prof -memprofile mem.prof -bench .

3. 程序当前的内存使用情况

用处:

查看当前内存占用,是否有未及时释放的内存,例如不断增长的slice或map;查看内存分配历史统计,是否有对象分配过于频繁,能否复用分配的对象。

通过http请求间接获取:

同第1点里面获取goroutine信息一样,在程序中引入包net/http/pprof并只使用它的初始化:

import _ "net/http/pprof"

然后使用DefaultServeMux来监听某一端口:

http.ListenAndServe(":9999", nil)

在程序里面添加上面的代码后,运行时我们可以通过浏览器或wget访问下面的http地址获取heap profile:

http://localhost:9999/debug/pprof/heap

这个http请求会立刻返回二进制格式的内容,我们把返回的内容保存为文件,假设取名为mem.prof, 这个文件里面包含了程序当前正在使用的内存的占用情况。

接下来我们通过在命令行输入go tool pprof来查看mem.prof:

go tool pprof mem.prof

这一句是查看分配之后还没有释放的内存(in-use memory):要么正在使用,要么确实没有及时释放;这对于检查内存泄漏很有帮助。

我们还可以在上面的命令加入几个选项:

-inuse_space      Display in-use memory size, default
-inuse_objects    Display in-use object counts
-alloc_space      Display allocated memory size
-alloc_objects    Display allocated object counts

不加上面任何选项时默认是使用-inuse_space, 如果我们想看程序从启动到现在所有分配过的内存(包含已经被释放了的),可以指定-alloc_space:

go tool pprof -alloc_space mem.prof

通过这个命令我们可以查看分配历史的统计,知道什么地方分配内存过于频繁,是否可以复用。

上面的这些命令会进入命令行交互,我们可以输入很多子命令来查看相关信息,最常用的几个子命令是:

top N

该子命令会列出N处分配内存最多的代码所在的函数,在输入top命令前输入cum或flat可以使得top列出的列表按cum或flat列排序,flat是单个函数自身(不计函数里面调用的其它函数)的内存占用,cum是函数总的占用。

如果要看某个函数具体是在什么地方分配的内存,可以使用list子命令查看:

list func_name_in_top_list

该命令会显示分配内存的代码和行号。

为了更直观的看出热点函数的调用关系,我们更多的是使用web子命令:

web

该命令会生成一个svg文件并用浏览器打开这个文件

上面是通过浏览器或wget获取mem.prof文件,然后使用go tool pprof命令来查看这个文件,更直接的方式是直接在命令行输入:

go tool pprof http://localhost:9999/debug/pprof/heap

这个命令会将http的返回写入临时文件,再用go tool pprof查看这个文件并进入命令行交互,过程和上面保存到自定义文件是一样的。

直接获取:

import "runtime/pprof"

...

f, err := os.Create(*memprofile)
if err != nil {
    log.Fatal("could not create memory profile: ", err)
}
runtime.GC() // get up-to-date statistics
if err := pprof.WriteHeapProfile(f); err != nil {
    log.Fatal("could not write memory profile: ", err)
}
f.Close()

上面的代码可以在程序的任何地方执行,输出的文件的内容就是程序当前的内存使用情况, 和前面通过http请求获取的mem.prof一样,我们通过go tool pprof来查看这个文件。

4. 一段时间内所有goroutine的调度和执行过程

用处:

前面的3点对于保证程序正常工作很有用,但是如果我们想提高程序的性能,例如在网络IO或数据库IO很大的情况下如何充分利用CPU来提高处理速度,这时前面3点就不够用了。我们需要了解各个goroutine是如何被调度的,如何同步执行的;网络等待,同步锁等待,还有系统调用的等待都发生在什么地方,在这些等待的地方是不是有CPU空闲被浪费了,这些信息都在trace里面。

通过http请求间接获取:

同第1点里面获取goroutine信息一样,在程序中引入包net/http/pprof并只使用它的初始化:

import _ "net/http/pprof"

然后使用DefaultServeMux来监听某一端口:

http.ListenAndServe(":9999", nil)

在程序里面添加上面的代码后,运行时我们可以通过浏览器或wget访问下面的http地址获取trace信息:

http://localhost:9999/debug/pprof/trace?seconds=10

这个http请求会等待seconds秒,这期间读取请求返回的二进制格式内容,我们把返回的内容保存为文件,假设取名为trace.out, 这个文件里面包含了这段时间内的trace信息。

接下来我们通过在命令行输入go tool trace来查看trace.out:

go tool trace trace.out

这个命令会开启一个web服务,然后打开浏览器运行一个web app,显示有如下title:

View trace
Goroutine analysis
Network blocking profile
Synchronization blocking profile
Syscall blocking profile
Scheduler latency profile

点击各个title都可以进入查看详细信息,其中"View trace"是一个UI交互界面,可以按时间轴查看各个goroutine在各个OS thread上的执行顺序,关于这个web app的详细使用方式可以参考这篇文章

直接获取:

上面通过http请求间接获取的方式实际上最终是url的handler调用了runtime/trace包的接口

import "runtime/trace"

func Trace(w http.ResponseWriter, r *http.Request) {
    sec, err := strconv.ParseFloat(r.FormValue("seconds"), 64)
    if sec <= 0 || err != nil {
        sec = 1
    }

    if durationExceedsWriteTimeout(r, sec) {
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.Header().Set("X-Go-Pprof", "1")
        w.WriteHeader(http.StatusBadRequest)
        fmt.Fprintln(w, "profile duration exceeds server's WriteTimeout")
        return
    }

    // Set Content Type assuming trace.Start will work,
    // because if it does it starts writing.
    w.Header().Set("Content-Type", "application/octet-stream")
    if err := trace.Start(w); err != nil {
        // trace.Start failed, so no writes yet.
        // Can change header back to text content and send error code.
        w.Header().Set("Content-Type", "text/plain; charset=utf-8")
        w.Header().Set("X-Go-Pprof", "1")
        w.WriteHeader(http.StatusInternalServerError)
        fmt.Fprintf(w, "Could not enable tracing: %s\n", err)
        return
    }
    sleep(w, time.Duration(sec*float64(time.Second)))
    trace.Stop()
}  

我们可以在程序里面直接调用runtime/trace包的接口:

import "runtime/trace"

var traceprofile = flag.String("traceprofile", "", "write trace profile `file`")

func main() {
    flag.Parse()
    if *traceprofile != "" {
        f, err := os.Create(*traceprofile)
        if err != nil {
            log.Fatal("could not create trace profile: ", err)
        }
        if err := trace.Start(f); err != nil {
            log.Fatal("could not start trace profile: ", err)
        }
        defer trace.Stop()
    }

    // ... rest of the program ...
 }

上面的代码trace profile的周期是从程序启动到退出,profile的内容保存到traceprofile参数指定的文件,和前面通过http请求获取的trace.out一样,我们通过go tool trace来查看这个文件。

另外可参考下面的文章做深入了解:

golang 内存分析/动态追踪

Profiling Go programs with pprof

Custom pprof profiles

Mutex profile

pprof user interface

Go execution tracer

go tool trace

An Introduction to go tool trace

Go Execution Tracer(design doc)

Go, the unwritten parts


有疑问加站长微信联系(非本文作者))

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

11109 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传