685 字
3 分钟
pprof 是采样器,不是记账本

第一次用 heap profile 查内存问题时,我盯着 top 里的数字和监控面板上的 RSS 对不上,差了将近一倍,一度以为工具坏了。后来才明白:pprof 看到的是采样,不是精确账本。

CPU profile 的 100Hz#

运行时默认以 100Hz 的频率中断采样,记录当前调用栈。也就是说,一个只运行 3ms 的函数,被采到的概率大约三成;大量短函数叠加起来,单个函数可能一次都不出现。

这带来两个后果:

  • 热点排序是统计意义上的。排名靠前的函数通常确实热,但排名靠后的不代表不热;
  • 采样的开销会改变被测程序。把频率调到 1000Hz 能看清短函数,但 CPU 时间也随之上去。
f, _ := os.Create("cpu.pprof")
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()

如果怀疑的是短函数,别只调频率,先看调用链——一个短函数被高频调用时,它的父函数通常会出现在样本里。

heap profile 的 512KB#

堆采样默认每分配 512KB 才记录一次。小于这个阈值的对象,只有累积到一定程度才可能被采到。一个每次泄漏 200 字节、每秒执行一万次的服务,在 profile 里可能完全看不见,但一小时就是 7GB。

// 临时调试:让每次分配都记录。开销很大,只在小流量环境用。
old := runtime.MemProfileRate
runtime.MemProfileRate = 1
defer func() { runtime.MemProfileRate = old }()

另外要分清 alloc_space 和 inuse_space:

  • alloc_space 是累计分配量,适合找“分配大户”;
  • inuse_space 是当前存活量,适合找泄漏。

找泄漏的正确姿势是隔一段时间抓两次 inuse_space,在 pprof 里用 -base 相减,再按差值排序。单张快照只能告诉你“现在谁占着”,不能告诉你“谁在涨”。

block 和 mutex 默认是关的#

这两类 profile 默认不采样,需要显式打开:

runtime.SetBlockProfileRate(1) // 每次阻塞都记录,代价高
runtime.SetMutexProfileFraction(10) // 记录 1/10 的锁竞争

它们回答的是“时间花在等谁”,而不是“时间花在算什么”。goroutine 泄漏、连接池排队、channel 满导致的阻塞,通常只能在这里看到。

goroutine profile 只是一张快照#

pprof.Lookup("goroutine") 抓到的是某一瞬间的栈。如果泄漏的 goroutine 在周期性地创建和退出,单次快照可能正好错过。抓三次、间隔几十秒,比较栈的数量变化,比看一次更可靠。

结论#

用 pprof 之前先问三个问题:这个 profile 的采样率是多少、它记录的是累计还是当前、样本量够不够。把采样结果当成精确值,是比不会用 pprof 更常见的错误。

pprof 是采样器,不是记账本
/posts/go-pprof-sampling/
作者
fantasy-peak
发布于
2026-10-27
许可协议
CC BY-NC-SA 4.0