Go 性能剖析:用 pprof 找热点

AI 辅助生成本文由 AI 起草并经人工核查,示例均标明软件版本;发现错误请以官方文档为准。

Go 1.24

「这个接口有点慢」不是结论。性能工作要从一个可复现的测量开始,再到 profile 里找到真正吃 CPU 或占内存的那几行代码。pprof 是 Go 自带的答案,不需要装任何第三方库。

环境准备

只需要 Go 1.24 与 go tool pprof(随工具链分发)。benchstat 用来比较两次基准结果,单独安装:

go install golang.org/x/perf/cmd/benchstat@latest

确认 $(go env GOPATH)/bin 在 PATH 里,否则 benchstat 找不到。

先有一个能重复跑的基准:

package parse

import (
	"strings"
	"testing"
)

func Parse(line string) (string, string) {
	key, value, ok := strings.Cut(line, "=")
	if !ok {
		return "", ""
	}
	return strings.TrimSpace(key), strings.TrimSpace(value)
}

func BenchmarkParse(b *testing.B) {
	line := "  user_id = 42  "
	b.ReportAllocs()

	for b.Loop() {
		Parse(line)
	}
}

Go 1.24 的 b.Loop() 会保证循环体里的调用不被编译器优化掉,也自动处理计时器的启停;旧写法 for i := 0; i < b.N; i++ 仍可用,但需要自己防止结果被内联消除。

go test -bench BenchmarkParse -benchmem ./parse

-benchmem 会额外报告 B/op 与 allocs/op,内存问题往往先在这里露头。

采集 profile

基准测试可以直接产出 profile,不需要改业务代码:

go test -bench BenchmarkParse -benchmem \
  -cpuprofile cpu.out -memprofile mem.out ./parse

生成两个文件:cpu.out 是 CPU 采样,mem.out 是堆分配采样。几个注意点:

  • 默认采样频率足够定位热点,不要为了「更准」把 -memprofilerate 调到 1,那会让基准慢一个数量级。
  • 内存 profile 记录的是采样时刻的堆状态。想让 inuse_space 稳定,采集前先 runtime.GC()。
  • 单次基准的 profile 只代表这一次运行,先确认 -benchtime 够长(默认 1s 通常够用)。

读懂 pprof

go tool pprof cpu.out

进入交互模式,最常用的四个命令:

(pprof) top
(pprof) top -cum
(pprof) list Parse
(pprof) web
  • top 按 flat(函数自身耗时,不含子调用)排序,直接指向最热的代码。
  • top -cum 按 cumulative(含子调用)排序,用来看哪条调用链最贵。
  • list Parse 显示该函数的逐行耗时,前提是二进制带调试信息。
  • web 生成调用图,需要 Graphviz 的 dot;没装就用 -http。

不装 Graphviz 也能看图形界面:

go tool pprof -http=:8080 cpu.out

浏览器会打开 Graph、Flame Graph、Source 三个视图。火焰图横轴是栈的合并宽度(越宽表示采样越多),纵轴是调用深度;找「宽而平」的帧——那是自身耗时高的函数,而不是被它调用的子函数。

一个常见误读:火焰图上层的函数宽,不代表它慢,可能只是被调用得多。结合 top 的 flat 排序一起看。

内存与分配

go tool pprof -sample_index=alloc_space mem.out

默认视图是 inuse_space(当前存活对象占用),另外三个索引常用来区分问题:

-sample_index 含义
inuse_space 采样时仍存活的对象占用字节
inuse_objects 仍存活的对象数量
alloc_space 累计分配字节(含已回收)
alloc_objects 累计分配对象数

「内存一直涨」看 inuse_space;「GC 频繁、CPU 被 GC 吃掉」看 alloc_space 与 allocs/op。

比较两次 profile 可以直接看差异:

go tool pprof -diff_base before.out after.out

正数表示改动后变多,负数表示变少。比肉眼对比两份 top 可靠得多。

生产环境采样

标准库自带 net/http/pprof,匿名导入即可把端点注册到 http.DefaultServeMux:

package main

import (
	"log"
	"net/http"
	_ "net/http/pprof"
)

func main() {
	go func() {
		// 只监听回环地址,不要用 0.0.0.0
		log.Println(http.ListenAndServe("127.0.0.1:6060", nil))
	}()

	// 业务服务
	_ = http.ListenAndServe(":8080", nil)
}

常用端点:

curl 'http://127.0.0.1:6060/debug/pprof/profile?seconds=30' -o cpu.out
curl 'http://127.0.0.1:6060/debug/pprof/heap' -o heap.out
curl 'http://127.0.0.1:6060/debug/pprof/goroutine?debug=1'

seconds=30 是 CPU 采样时长,期间会有额外开销,别在流量高峰拉长时间。goroutine?debug=1 输出全部栈,排查 goroutine 泄漏比 profile 更直接。

安全的做法:

  • 只监听 127.0.0.1,通过 SSH 隧道或内网访问,绝不挂到公网 mux。
  • 或者用独立端口 + 防火墙白名单,不与业务端口共用。
  • 生产二进制若用 -trimpath -s -w 构建,list 可能定位不到源码,排障版本单独保留符号。

阻塞与锁竞争需要先开采样,默认是关闭的:

runtime.SetBlockProfileRate(1)
runtime.SetMutexProfileFraction(1)

1 表示每次都记录,开销明显,只在排障时打开;对应端点是 /debug/pprof/block 与 /debug/pprof/mutex。

用 benchstat 确认改动有效

单次 go test -bench 的数字噪声很大,至少跑 10 次取统计量:

go test -run '^$' -bench . -count=10 -benchmem ./parse > old.txt
# 修改代码
go test -run '^$' -bench . -count=10 -benchmem ./parse > new.txt
benchstat old.txt new.txt

-run '^$' 跳过普通测试,只跑基准。benchstat 会给出中位数、变化百分比与 geomean,并标注差异是否显著。

Windows 上注意重定向编码:PowerShell 5 的 > 默认写 UTF-16,benchstat 读不了。用 cmd /c "go test ... > old.txt",或 | Out-File -Encoding utf8 old.txt。

判定标准要事先定:优化目标是 20% 以上,低于噪声区间的「提升」不值得合并。

常见错误与注意事项

1. 基准里结果没被使用

编译器会把没有副作用的调用优化掉,测出来接近 0。用 b.Loop(),或把结果赋给包级 var Sink any。

2. 用一次运行下结论

CPU 频率、调度、其他进程都会影响结果。-count=10 + benchstat 是底线。

3. 把 -benchtime 调得太短

样本太少,profile 里全是噪声。默认 1s 起步,热点不明显再加大。

4. 在生产长时间开 CPU profile

采样有开销,30 秒是常见上限;高峰期限流或改到低峰执行。

5. 把 /debug/pprof 暴露到公网

它相当于一个远程诊断后门,能看到内存内容与调用栈。只绑回环地址。

6. 只看 CPU 不看分配

GC 压力常常才是延迟来源。-benchmem、alloc_space 与 GOGC 一起看。

7. 在共享 CI 机器上比绝对值

虚拟机的 CPU 配额随时变化。CI 里只比同一台机器上的前后差异,或只看趋势。

8. 优化没有测量支撑的地方

凭直觉改代码,往往只是把开销挪到另一处。先 profile,再改,再用 benchstat 验证。

小结

  • 先有可复现的基准(b.Loop() + -benchmem),再谈优化。
  • -cpuprofile / -memprofile 从测试产出 profile;生产用 net/http/pprof,只绑回环地址。
  • top 看 flat 热点,top -cum 看调用链,list 看行级,火焰图看整体形状。
  • 内存看 -sample_index:泄漏用 inuse_space,GC 压力用 alloc_space。
  • 改动是否有效由 benchstat 判定,-count=10 起步。

下一步