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.MemProfileRateruntime.MemProfileRate = 1defer 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 更常见的错误。