go程序从开始运行起,runtime就按照一定频率对内存分配进行采样记录:当内存分配每达到一定值(默认是512KB,由[`runtime.MemProfileRate`](https://golang.google.cn/pkg/runtime/#pkg-variables)设定), runtime就会记录下当前这次内存分配的大小,call stack等信息到profile里面。
而CPU的profile是从调用[`runtime/pprof包的pprof.StartCPUProfile()`](https://golang.google.cn/pkg/runtime/pprof/#StartCPUProfile)开始,到[`pprof.StopCPUProfile()`](https://golang.google.cn/pkg/runtime/pprof/#StopCPUProfile)结束,每1秒采样100次,每次采样时记录下当前正在执行的所有goroutine的call stack,某个函数在这些快照里面出现的次数越多就说明这个函数越耗时。
运行的go程序的所有profile信息都是通过在运行时调用[`runtime/pprof`](https://golang.google.cn/pkg/runtime/pprof/)和[`runtime/trace`](https://golang.google.cn/pkg/runtime/trace/)两个包的接口获取,调用这些接口的方式有直接调用和通过http请求间接调用两种,下面我们说明各种常用的profile信息以及它们的获取,使用方式。
#### 1. **当前所有goroutine的函数调用栈**
**用处:**
查看当前有哪些goroutine在运行,数量对不对,有没有出现很多goroutine卡在同一个地方的情况,有没有死锁的情况,间隔的刷新页面看看各个goroutine数量变化情况,了解程序运行状况变化。
**通过http请求间接获取:**
在程序中引入包[`net/http/pprof`](https://golang.google.cn/pkg/net/http/pprof/)并只使用它的初始化:
import _ "net/http/pprof"
这一句导致[`源码里面实际执行的是`](https://github.com/golang/go/blob/master/src/net/http/pprof/pprof.go#L71):
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里面`](https://github.com/golang/go/blob/master/src/net/http/server.go#L2374),所以要想使用这些url就必须使用DefaultServeMux来监听某一端口:
http.ListenAndServe(":9999", nil)
这里第一个参数可以是本机的任一端口,[`当第二个参数为nil时就使用http包的DefaultServeMux来监听并处理http请求`](https://golang.google.cn/pkg/net/http/#ListenAndServe), 或者当程序里面已经有自定义的ServeMux时,可以像上面的init函数那样把pprof的处理函数[`注册到自定义的ServeMux`](https://golang.google.cn/pkg/net/http/#ServeMux.HandleFunc)。
在程序里面添加上面的代码后,运行时我们可以通过浏览器或wget访问下面的http地址查看goroutine信息:
http://localhost:9999/debug/pprof/goroutine?debug=1
或者进入index页面查看更多项:
http://localhost:9999/debug/pprof
**直接获取:**
不常用,可以查看[`源码`](https://github.com/golang/go/blob/master/src/net/http/pprof/pprof.go#L224)
#### 2. **一段时间内的CPU profile**
**用处:**
查看消耗CPU的热点函数,能否减少调用次数,能否避免重复计算。
**通过http请求间接获取:**
同第1点里面获取goroutine信息一样,在程序中引入包[`net/http/pprof`](https://golang.google.cn/pkg/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`](https://golang.google.cn/cmd/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`](https://golang.google.cn/cmd/pprof/)命令来分析这个文件,更直接的方式是直接在命令行输入:
go tool pprof http://localhost:9999/debug/pprof/profile?seconds=30
这个命令会等seconds指定的秒数,这期间读取请求返回的二进制格式内容并写入临时文件,再用[`go tool pprof`](https://golang.google.cn/cmd/pprof/)查看这个文件并进入命令行交互,过程和上面保存到自定义文件是一样的。
**直接获取:**
上面通过http请求间接获取的方式实际上最终还是[`url的handler调用了runtime/pprof包的接口`](https://github.com/golang/go/blob/master/src/net/http/pprof/pprof.go#L105):
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`](https://golang.google.cn/pkg/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`](https://golang.google.cn/cmd/pprof/)来查看这个文件。
除了像上面那样在程序中加profile的代码,`go test`命令自带profile功能,可以在运行`go test`命令时指定[`profile相关的flag`](https://golang.google.cn/cmd/go/#hdr-Description_of_testing_flags),例如下面的命令运行当前目录下的测试用例并将cpu和heap profile写到指定的文件,然后我们就可以用[`go tool pprof`](https://golang.google.cn/cmd/pprof/)来查看这些profile文件了:
go test -cpuprofile cpu.prof -memprofile mem.prof -bench .
#### 3. **程序当前的内存使用情况**
**用处:**
查看当前内存占用,是否有未及时释放的内存,例如不断增长的slice或map;查看内存分配历史统计,是否有对象分配过于频繁,能否复用分配的对象。
**通过http请求间接获取:**
同第1点里面获取goroutine信息一样,在程序中引入包[`net/http/pprof`](https://golang.google.cn/pkg/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`](https://golang.google.cn/cmd/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`](https://golang.google.cn/cmd/pprof/)命令来查看这个文件,更直接的方式是直接在命令行输入:
go tool pprof http://localhost:9999/debug/pprof/heap
这个命令会将http的返回写入临时文件,再用[`go tool pprof`](https://golang.google.cn/cmd/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`](https://golang.google.cn/cmd/pprof/)来查看这个文件。
#### 4. **一段时间内所有goroutine的调度和执行过程**
**用处:**
前面的3点对于保证程序正常工作很有用,但是如果我们想提高程序的性能,例如在网络IO或数据库IO很大的情况下如何充分利用CPU来提高处理速度,这时前面3点就不够用了。我们需要了解各个goroutine是如何被调度的,如何同步执行的;网络等待,同步锁等待,还有系统调用的等待都发生在什么地方,在这些等待的地方是不是有CPU空闲被浪费了,这些信息都在trace里面。
**通过http请求间接获取:**
同第1点里面获取goroutine信息一样,在程序中引入包[`net/http/pprof`](https://golang.google.cn/pkg/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`](https://golang.google.cn/cmd/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的详细使用方式可以参考[`这篇文章`](https://blog.gopheracademy.com/advent-2017/go-execution-tracer/)。
**直接获取:**
上面通过http请求间接获取的方式实际上最终是[`url的handler调用了runtime/trace包的接口`](https://github.com/golang/go/blob/master/src/net/http/pprof/pprof.go#L139):
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`](https://golang.google.cn/pkg/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`](https://golang.google.cn/cmd/trace/)来查看这个文件。
#### **另外可参考下面的文章做深入了解:**
[golang 内存分析/动态追踪](https://lrita.github.io/2017/05/26/golang-memory-pprof/)
[Profiling Go programs with pprof](https://jvns.ca/blog/2017/09/24/profiling-go-with-pprof/)
[Custom pprof profiles](https://rakyll.org/custom-profiles/)
[Mutex profile](https://rakyll.org/mutexprofile/)
[pprof user interface](https://rakyll.org/pprof-ui/)
[Go execution tracer](https://blog.gopheracademy.com/advent-2017/go-execution-tracer/)
[go tool trace](https://making.pusher.com/go-tool-trace/)
[An Introduction to go tool trace
](https://about.sourcegraph.com/go/an-introduction-to-go-tool-trace-rhys-hiltner/)
[Go Execution Tracer(design doc)](https://docs.google.com/document/d/1FP5apqzBgr7ahCCgFO-yoVhk4YZrNIDNf9RybngBc14/edit#heading=h.ayzntospglb3)
[Go, the unwritten parts](https://rakyll.org/archive/)
有疑问加站长微信联系(非本文作者))