第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命令
  • 分析时多用toplistweb命令,配合火焰图效果更佳

下一章,我们会深入讲解如何用pprof定位具体的性能瓶颈,包括CPU密集型、内存泄漏、goroutine泄漏等实战案例。到时候我会分享一些我在线上踩过的坑,保证让你少走弯路。