Go pprof 入门:给 HTTP 服务找 CPU 和内存热点

本文讲解 Go pprof 在 HTTP 服务中的基本使用方式,包括启用调试端口、采集 CPU profile、查看内存分配和安全注意事项。

性能问题要靠数据,不靠感觉

服务慢了以后,很多人第一反应是猜:是不是 JSON 慢?是不是数据库慢?是不是某个循环太多?猜测可以提供方向,但真正优化前要有证据。Go 的 pprof 可以帮助你看到 CPU 时间花在哪里、内存分配集中在哪里、goroutine 是否堆积。

pprof 不是只给性能专家用。入门者只要掌握几个基本命令,就能从“感觉很慢”走向“这个函数确实占了 40% CPU”。这篇文章用 HTTP 服务讲最常见的启用和查看方式。

启用 pprof 端点

导入:

import _ "net/http/pprof"

启动一个单独调试端口:

go func() {
	log.Println("pprof listening on localhost:6060")
	if err := http.ListenAndServe("localhost:6060", nil); err != nil {
		log.Printf("pprof server: %v", err)
	}
}()

注意这里绑定 localhost。不要把 pprof 直接暴露到公网。pprof 可能泄露路径、函数名、内存信息和运行状态,应该只在本机或受控内网访问。

访问:

http://localhost:6060/debug/pprof/

你会看到 goroutine、heap、profile 等入口。

采集 CPU profile

运行服务后执行:

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

进入交互后:

top

查看最耗 CPU 的函数。也可以:

list FunctionName

查看某个函数内具体行。生成图形需要本机有 graphviz 等工具,入门阶段先用 toplist 就足够。

如果你看到大部分 CPU 都在某个 JSON 转换函数、模板渲染函数或字符串拼接函数,就有了优化方向。没有 profile 前,不要随便改代码。

查看内存分配

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

进入后:

top

heap profile 可以帮助你发现哪些地方分配多。比如某个 handler 每次请求都读取巨大文件、重复解析模板、创建大量临时切片。

一个常见优化是把模板解析放到启动阶段:

tmpl, err := template.ParseFS(templateFS, "templates/*.html")
if err != nil {
	log.Fatal(err)
}

而不是每个请求都解析:

// 不推荐
func handler(w http.ResponseWriter, r *http.Request) {
	tmpl := template.Must(template.ParseFiles("templates/index.html"))
	tmpl.Execute(w, data)
}

pprof 能帮你确认这种优化是否真的影响热点。

goroutine 堆积

查看 goroutine:

curl http://localhost:6060/debug/pprof/goroutine?debug=1

如果 goroutine 数量越来越多,可能有泄漏。常见原因包括 channel 没人接收、后台循环没有退出、HTTP 请求没有超时、没有关闭响应体。

比如外部请求忘记关闭 body:

resp, err := http.Get(url)
if err != nil {
	return err
}
defer resp.Body.Close()

如果没有 Close,连接资源可能无法复用,长期运行会出问题。

安全和环境边界

pprof 很有用,但不要默认公开。建议:

  • 绑定 localhost
  • 只在调试环境开启
  • 生产环境放在内网并加访问控制
  • 不要把 pprof 路由挂到公网主服务上

也可以通过配置控制:

if cfg.EnablePprof {
	go startPprofServer()
}

配置名要明确,让开启 pprof 成为有意识的动作。

从一次慢接口排查开始

假设有个导出接口偶尔要跑十几秒,日志只告诉你“请求很慢”,但不知道慢在哪里。比较务实的流程是先在压测或预发环境复现,再采集 CPU profile:

go tool pprof http://127.0.0.1:6060/debug/pprof/profile?seconds=30

进入交互界面后先看 top,找出占比最高的函数。如果热点落在 JSON 编码、模板渲染、压缩、数据库扫描或某个循环里,再用 list 函数名 看具体行。不要看到一个函数在前面就立刻改,它可能只是调用链的汇合点。结合业务日志、输入规模和调用次数一起看,结论会更稳。

内存问题也类似。先确认是“瞬时内存高”还是“长期不下降”。瞬时内存高可能是一次性加载太多数据,长期不下降则可能是缓存没有淘汰、全局 map 持续增长或 goroutine 泄漏。pprof 能告诉你对象从哪里分配,但是否属于泄漏,需要结合程序生命周期判断。入门阶段能把“先采样、再定位、最后验证”这条路径跑通,就已经比凭感觉优化可靠很多。

常见误读:看到分配不等于必须清零

pprof 很容易让人进入一种紧张状态:只要看到某个函数在分配内存,就想把它改到零分配。这个目标听起来漂亮,但在普通业务服务里未必划算。比如一次后台导出需要构造几万个结构体,这是业务本身需要的内存;真正要看的是它是否在请求结束后释放,是否导致 GC 压力过高,是否可以通过分页或流式写出降低峰值。

CPU profile 也一样。某个函数占比高,可能只是因为它做了最多的正常工作。比如 JSON 编码出现在顶部,不一定表示编码器有问题,也可能是你一次返回的数据太多。优化方向也许不是“换一个更快的 JSON 库”,而是限制导出字段、分页返回、异步生成文件,或者让前端避免频繁刷新。

一个比较可靠的习惯是每次只改一个点,然后重新采样。比如先把一次性查询改成分页,再看 heap 峰值是否下降;再把字符串拼接改成 strings.Builder,看 CPU 占比是否变化。多项修改混在一起时,最后即使变快了,也很难知道到底是哪一项起作用。pprof 给的是证据链,不是一次性的判决书。

保存 profile 方便对比

本地排查时,可以把采样结果保存下来:

go tool pprof -output cpu.pb.gz http://127.0.0.1:6060/debug/pprof/profile?seconds=30

改完代码后再保存一份新的 profile。这样你可以回头比较两次热点是否真的变化,而不是靠记忆判断。团队协作时,把 profile 文件、压测命令、关键结论一起记录在 issue 或文档里,也能让别人复现你的分析过程。

不要把 profile 当成神秘工具。它本质上是在程序运行时抽样,告诉你时间和内存大概花在了哪里。抽样会有误差,所以需要足够长的采集时间和相对稳定的负载。一次 3 秒的 profile 可能只能看个大概,排查接口热点时常用 30 秒或 60 秒会更稳。

采样时也要记下当时的请求量、输入规模和机器配置。没有这些背景,profile 文件只能说明“那一刻发生了什么”,很难支持后续复盘和团队讨论。

这些记录会让优化结论更可信。

小结

pprof 能帮助 Go HTTP 服务定位 CPU、内存和 goroutine 问题。基本流程是:受控开启 pprof 端口,用 go tool pprof 采集 CPU 或 heap,用 toplist 找热点,再做针对性优化。

性能优化不要靠感觉。先测量,再修改,再验证。pprof 是 Go 工具链里非常值得入门掌握的一环。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页