第2章:pprof性能剖析入门
性能优化这件事,说白了就是先找到瓶颈在哪。
你想想看,一个Go服务跑得慢,到底是CPU扛不住了,还是内存吃紧了?又或者是goroutine调度出了问题?没有数据支撑,全靠猜,那跟闭着眼修车有什么区别。
pprof就是Go官方给我们的一把「手术刀」。它能精准地告诉你,你的程序到底慢在哪、卡在哪、内存都花在哪了。我个人习惯,每个线上服务都必须集成pprof,就像汽车必须装仪表盘一样自然。
2.1 pprof是什么?
pprof是Go标准库自带的性能剖析工具。它全称是「profiling profiler」,专门用来采集和分析程序的运行时数据。
它能干什么?我列几个最常用的场景:
- CPU profile:看看哪些函数在「吃」CPU时间
- Memory profile:看看哪些地方分配了堆内存,有没有泄漏
- Goroutine profile:看看当前有多少goroutine在跑,有没有泄漏
- Block profile:看看goroutine在哪些地方阻塞了
- Mutex profile:看看锁竞争严重不严重
嗯,这里要注意,pprof采集的是采样数据,不是精确计数。它通过定期中断程序来记录调用栈,所以对性能影响很小,生产环境也能用。
核心概念:pprof基于「采样」原理。比如CPU profile,默认每秒采样100次(即每10ms一次),记录当前正在执行的函数调用栈。采样次数越多,统计越准确,但开销也越大。
2.2 如何集成到HTTP服务?
集成pprof到HTTP服务,其实就一行代码的事。但很多新手会踩坑,我当年也犯过这个错。
标准做法是导入net/http/pprof包:
import (
"net/http"
_ "net/http/pprof" // 注册pprof路由
)
func main() {
// 你的业务路由
http.HandleFunc("/api/user", userHandler)
// pprof路由会自动注册到默认的ServeMux上
// 访问 /debug/pprof/ 就能看到所有profile的入口
log.Fatal(http.ListenAndServe(":8080", nil))
}
就这么简单。导入这个包后,它会自动在/debug/pprof/路径下注册一系列端点:
| 端点路径 | 说明 |
|---|---|
/debug/pprof/ |
pprof首页,列出所有可用的profile |
/debug/pprof/profile |
采集CPU profile,默认30秒 |
/debug/pprof/heap |
采集堆内存profile |
/debug/pprof/goroutine |
查看当前所有goroutine的调用栈 |
/debug/pprof/block |
查看goroutine阻塞情况 |
/debug/pprof/mutex |
查看锁竞争情况 |
安全警告:千万不要在生产环境直接暴露/debug/pprof/!我曾经见过有人直接把pprof暴露到公网,结果被人扫到,直接dump了内存数据,里面全是敏感信息。
建议做法:用单独的端口监听pprof,或者加IP白名单、Basic Auth认证。
如果你用的是gin、echo这类框架,集成方式也类似。以gin为例:
import (
"github.com/gin-gonic/gin"
"net/http/pprof"
)
func main() {
r := gin.Default()
// 手动注册pprof路由
r.GET("/debug/pprof/", gin.WrapH(pprof.Index))
r.GET("/debug/pprof/heap", gin.WrapH(pprof.Handler("heap")))
r.GET("/debug/pprof/goroutine", gin.WrapH(pprof.Handler("goroutine")))
r.GET("/debug/pprof/block", gin.WrapH(pprof.Handler("block")))
r.GET("/debug/pprof/mutex", gin.WrapH(pprof.Handler("mutex")))
// CPU profile需要单独处理
r.GET("/debug/pprof/profile", gin.WrapH(pprof.Handler("profile")))
r.Run(":8080")
}
2.3 采集CPU和内存profile
集成好之后,怎么采集数据呢?有两种方式:
方式一:浏览器直接访问
打开浏览器,访问http://localhost:8080/debug/pprof/,你会看到一个简单的页面,上面有各种profile的链接。点击就能下载对应的profile文件。
方式二:用go tool pprof命令行
这才是我们日常用的方式。我个人习惯用命令行,因为可以边采集边分析:
# 采集CPU profile,默认30秒
go tool pprof http://localhost:8080/debug/pprof/profile
# 指定采集时长,比如60秒
go tool pprof http://localhost:8080/debug/pprof/profile?seconds=60
# 采集堆内存profile
go tool pprof http://localhost:8080/debug/pprof/heap
# 采集goroutine信息
go tool pprof http://localhost:8080/debug/pprof/goroutine
执行这些命令后,pprof会先下载profile数据,然后自动进入交互式命令行界面。
小技巧:采集CPU profile时,建议在服务负载较高的时候进行。如果服务很空闲,采出来的数据没什么参考价值。我一般会用wrk或hey先压一下服务,同时采集profile。
2.4 使用go tool pprof交互式分析
数据采完了,怎么分析?这才是重头戏。
进入交互式界面后,你会看到(pprof)提示符。常用的命令有这些:
| 命令 | 说明 |
|---|---|
top |
显示CPU占用最高的函数(按采样次数排序) |
list <函数名> |
查看某个函数的源码级别的耗时分布 |
web |
生成调用图并在浏览器中打开(需要安装graphviz) |
peek <函数名> |
查看函数的调用者和被调用者 |
traces |
显示所有采样到的调用栈 |
svg |
生成SVG格式的调用图 |
quit |
退出交互式界面 |
举个例子,假设我们采集了CPU profile,进入交互界面后:
(pprof) top
Showing nodes accounting for 3.50s, 87.50% of 4.00s total
Dropped 12 nodes (cum <= 0.02s)
Showing top 10 nodes out of 48
flat flat% sum% cum cum%
1.20s 30.00% 30.00% 1.50s 37.50% encoding/json.(*Decoder).Decode
0.80s 20.00% 50.00% 0.80s 20.00% runtime.memmove
0.50s 12.50% 62.50% 0.50s 12.50% runtime.mallocgc
...
看到没?flat列表示该函数自身消耗的CPU时间,cum列表示该函数及其调用的所有子函数消耗的总时间。这里json.Decode占了30%的CPU时间,说明JSON解析可能是瓶颈。
然后我们可以用list命令深入看看:
(pprof) list json.Decode
Total: 4.00s
ROUTINE ======================== encoding/json.(*Decoder).Decode
1.20s 1.50s (flat, cum) 37.50% of Total
. . 45: func (d *Decoder) Decode(v interface{}) error {
. . 46: if d.err != nil {
. . 47: return d.err
. . 48: }
0.30s 0.30s 49: d.scan.reset()
0.50s 0.50s 50: d.scanWhile(scanContinue)
0.40s 0.70s 51: return d.unmarshal(v)
. . 52: }
嗯,这里要注意,list命令会显示每一行代码的耗时。如果某一行特别耗时,那就是优化的重点。
我的经验:分析profile时,别只盯着top看。我建议先用web命令生成调用图,从整体上看看函数调用关系。有时候瓶颈不在最耗时的函数本身,而在它调用的某个子函数里。调用图能帮你快速定位「罪魁祸首」。
另外,go tool pprof还支持直接生成火焰图:
# 生成火焰图(需要安装FlameGraph工具)
go tool pprof -http=:8081 http://localhost:8080/debug/pprof/profile
这个命令会启动一个Web界面,在浏览器里打开http://localhost:8081,就能看到交互式的火焰图。我个人觉得火焰图比调用图更直观,一眼就能看出哪个函数「烧」得最旺。
避坑指南:我曾经在生产环境采集CPU profile时,发现采样结果里全是runtime相关的函数,业务代码几乎看不到。后来才发现,是因为采样时长太短(默认30秒),而服务刚启动时GC很频繁。建议在服务稳定运行一段时间后再采集,或者把采样时长延长到60秒以上。
好了,这一章的内容就这些。总结一下:
- pprof是Go官方的性能剖析工具,基于采样原理
- 集成到HTTP服务只需导入
net/http/pprof包 - 采集profile用
go tool pprof命令 - 分析时多用
top、list、web命令,配合火焰图效果更佳
下一章,我们会深入讲解如何用pprof定位具体的性能瓶颈,包括CPU密集型、内存泄漏、goroutine泄漏等实战案例。到时候我会分享一些我在线上踩过的坑,保证让你少走弯路。