背景
日常工作中,对程序运行情况进行分析,对现有功能重构改进,让程序更稳定高效,是我们日常工作中最重要的部分。而golang工具包中自带有pprof功能,让我们能方便找出程序中占用内存和cpu较多的罪魁祸首。
如何去分析?
大体分为两步:1. 收集对应数据 2. 分析对应数据 使用golang版本为go1.14.5 darwin/amd64
一、收集对应数据
使用go自带功能收集数据主要有以下三种方式,应用于不同的程序。使用方式如下:
1. runtime/pprof
人为的手动调用runtime.StartCPUProfile/runtime.StopCPUProfile等API来进行数据的采集。适合工具型应用,即运行一下就停止。
package main
import (
"fmt"
"os"
"runtime/pprof"
"time"
)
func main() {
cpuFile, err := os.Create("cpu.profile")
if err != nil {
fmt.Printf("create cpu profile error, error : %v\n", err)
os.Exit(0)
}
pprof.StartCPUProfile(cpuFile)
defer pprof.StopCPUProfile()
for i := 0; i < 10; i++ {
go block()
}
time.Sleep(10 * time.Second)
}
func block() {
var names chan string
for {
select {
case name := <-names:
fmt.Printf("name is : %s\n", name)
default:
}
}
}
执行go run 一下就可以生成cpu.profile文件用来分析。
其他运行情况支持如下:
2. net/http/pprof
这种方式是通过暴露http接口来获取对应的信息。观察net/http/pprof包内的init方法可以看到,内置定义了一些endpoint如下(go version:1.14.5):
package main
import (
"fmt"
"net/http"
_ "net/http/pprof"
)
func main() {
for i := 0; i < 10; i++ {
go block()
}
http.ListenAndServe("0.0.0.0:1988", nil)
}
func block() {
var names chan string
for {
select {
case name := <-names:
fmt.Printf("name is : %s\n", name)
default:
}
}
}
我们访问 http://127.0.0.1:1988/debug/pprof就可以得到对应的数据如下:
对应数据内容如下:
| Type | Desc |
|---|---|
| allocs | 过去分配内存的采样信息 |
| block | 阻塞的信息 |
| cmdline | 程序启动的命令行和参数 |
| goroutine | 所有协程的堆栈信息 |
| heap | 堆上内存分配的采样信息 |
| mutex | 锁竞争的信息 |
| profile | cpu 时间占用的信息 |
| threadcreate | 系统线程创建的信息 |
| trace | 程序运行的链路信息 |
当然,大部分情况下,线上服务器我们是访问不了的,我们可以通过wget获取对应的文件。例如获取最近60s内的cpu时间占用情况:
curl http://127.0.0.1:1988/debug/pprof/profile?seconds=60 -O cpu.profile
go test
Go自带了benchmark跑分库,可以对自己的软件构建跑分测试,方便在不同环境对性能进行方便的分析。 命令如下:
go test -v -run=^$ -bench=. -cpuprofile cpu.pprof
go test -v -run=^$ -bench=. -memprofile mem.pprof
go test -v -run=^$ -bench=. -mutexprofile mutex.pprof
运行当前目录下所有的基准测试,并且输出对应的性能文件
二、分析对应数据
分析数据主要依靠go tool里面的pprof工具。可以通过命令行方式处理,也能通过web方式展现。 核心命令如下:
go tool pprof <binary> <source>
binary 代表二进制文件路径 source 代表数据来源,可以是本地文件,也可以是http地址(例如前文的http://127.0.0.1:1988/debug/pprof/heap)
命令行方式
执行命令
go tool pprof ./heap.prof
进入交互式命令行界面:
Type: inuse_space 表示正在使用堆的内存情况
可以通过
go tool pprof -inuse_space ./heap.prof显示指定,支持以下选项:
- -inuse_space 当前占用堆的空间
- -inuse_obejcts 当前分配的对象个数
- -alloc_space 迄今占用的堆空间
- -alloc_objects 迄今分配的对象个数
接着执行top命令可以查看具体哪里占用堆空间比较多
top会列出5个统计数据:
flat: 本函数占用的内存量。
flat%: 本函数内存占使用中内存总量的百分比。
sum%: 前面每一行flat百分比的和,比如第2行虽然的100% 是 100% + 0%。
cum: 是累计量,假如main函数调用了函数f,函数f占用的内存量,也会记进来。
cum%: 是累计量占总量的百分比。
然后可以通过list functionName查看具体代码占用的位置
我们可以看到在 pprof.go文件的第749行占用了1.72M的内存,flat和cum也和上面展示的一样。
基本上排查可以利用 top > list functionName 处理大多数问题
web方式
执行命令
go tool pprof -http :611321 ./heap.profile
需要graphviz支持(主要用于支持火焰图),mac可以通过 brew install graphviz安装。
go 1.11之后支持火焰图展示了,其实不需要go torch来支持了。
进入web页面,可以看到如图下:
方块越大,表示占用的内存越高。
同时,左上部分的view支持以下维度查看:
- Top 默认查看前20占用比较高的堆
- Graph web服务默认界面(上图展示的)
- Flame Graph 火焰图
- Peek 显示所有链路的树行结构
- Source 显示具体代码行数,类似
list functionName
如何去定位
上述工具只能展示当时的heap分配情况,其实无法让我们精确定位到内存问题。一般而言,我们都应该以一个时间的快照文件为基点,同另外一个文件做比较,就可以轻松的发现内存问题。具体步骤如下:
- 通过
go tool pprof http://hostname:ipport/debug/pprof/heap命令分别在1分钟间隔内执行,可以获取到两个文件 xxx.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz 和 xxx.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz。 - 使用xxx.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz文件作为基准,查看一分钟内内存的变化量。
go tool pprof -base xxx.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz xxx.alloc_objects.alloc_space.inuse_objects.inuse_space.002.pb.gz
诚然我们能通过上述方式发现内存问题,但却不一定能找到内存泄露的问题。因为go程序中充斥了太多的goroutine,其中的关系调用可能很复杂,可能上述的内存增长过快的代码在一类协程g_leak中被调用,但是与此同时却有无数个协程调用产生g_leak。那么真实情况只有以下两种:
- g_leak只产生了几次,但是调用一次,内存就很大幅度上升,那么肯定是g_leak本身出了问题
- g_leak被调用了很多次,并且因为各种原因没有退出,消耗了巨大的内存。那么可能是调用g_leak的链路上出了问题,导致创建了大量的g_leak 第二种情况,就是goroutine导致的内存泄露。同样的,我们也可以通过在一定时间段内取pprof goroutine文件然后获取这段时间内增长过快的goroutine所处的堆栈。
一般而言,goroutine 导致内存泄露的情况一般都是创建过多的goroutine,但是没有正确结束(一般都是读写channel阻塞了,或者select阻塞了)
最后,推荐阅读下High Performance Go Workshop。Dave Cheney写的(Golang 开源贡献者和项目成员)。文章主要讨论怎么诊断go中的性能问题,并且如何修复。
原文链接: blog.zeromake.com/pages/high-…
译文链接: blog.zeromake.com/pages/high-…