heap profile 有 inuse 和 alloc 两种视角,内存泄漏和分配热点要看不同的列;它还是按采样得到的估计值,而且只反映最近一次 GC 时的状态。这篇用一个已知的程序验证这几点,并量采样率的精度与代价。
程序:garbageX 分配约 100 MB 的 1 KiB 切片但全部丢弃;retainY 分配并保留 50 个 1 MiB;retainZ 保留 16 万个 64 字节对象(约 10 MB);最后 runtime.GC() 再写 profile
-sample_index=inuse_space:
Type: inuse_space
Showing nodes accounting for 60.87MB, 100% of 60.87MB total
Showing top 6 nodes out of 25
flat flat% sum% cum cum%
46.26MB 76.00% 76.00% 46.26MB 76.00% main.retainY
12.10MB 19.88% 95.89% 12.10MB 19.88% main.retainZ
2.50MB 4.11% 100% 2.50MB 4.11% runtime.mallocgc
0 0% 100% 58.36MB 95.89% main.main
0 0% 100% 0.50MB 0.82% runtime.(*scavengerState).init
0 0% 100% 1MB 1.64% runtime.(*scavengerState).sleep
-sample_index=alloc_space:
Type: alloc_space
Showing nodes accounting for 174.92MB, 100% of 174.92MB total
Dropped 6 nodes (cum <= 0.87MB)
Showing top 6 nodes out of 20
flat flat% sum% cum cum%
98.10MB 56.08% 56.08% 98.10MB 56.08% main.garbageX
46.26MB 26.45% 82.53% 46.26MB 26.45% main.retainY
28.06MB 16.04% 98.57% 28.06MB 16.04% main.retainZ
2.50MB 1.43% 100% 2.50MB 1.43% runtime.mallocgc
0 0% 100% 172.41MB 98.57% main.main
0 0% 100% 1MB 0.57% runtime.(*scavengerState).sleepinuse_space 只算还活着的,alloc_space 算累计分配。丢弃的 98 MB 垃圾在后者里排第一、在前者里不出现。排查泄漏看 inuse,排查 GC 压力看 alloc。
retainY 在最近一次 GC 之后才分配 50 MiB:
不再 GC 直接写 profile:retainY 的 inuse_space = 20133 kB
runtime.GC() 之后再写:retainY 的 inuse_space = 54477 kB文档说 heap profile 报告最近一次完成的 GC 时的统计。实测分配 50 MiB 后不 GC 直接写,只看到 20133 kB(分配过程中触发过 GC,所以不是 0);再调用 runtime.GC() 后是 54477 kB。线上用 /debug/pprof/heap 取样时也要记得这点。
MemProfileRate= 1:retainZ 估计 最小 14200 kB / 中位 14200 kB / 最大 14200 kB MemProfileRate= 4096:retainZ 估计 最小 13936 kB / 中位 14119 kB / 最大 14464 kB MemProfileRate= 524288:retainZ 估计 最小 12394 kB / 中位 16490 kB / 最大 18538 kB MemProfileRate= 16777216:retainZ 估计 最小 49152 kB / 中位 83015 kB / 最大 98304 kB
默认每 512 KiB 采样一次,所以结果是估计:真实保留的 50 MiB,在第一次运行里读到 46.26 MB,在另一次运行里读到 54477 kB,小对象的波动更大。采样率 4096 时 6 次都在 13936–14464 kB,默认值是 12394–18538 kB,而 16 MiB 的采样率直接得到 49152–98304 kB——基本不可用。
MemProfileRate=0 分配 64 字节:11 ns/次 MemProfileRate=524288 分配 64 字节:10 ns/次 MemProfileRate=4096 分配 64 字节:17 ns/次 MemProfileRate=1 分配 64 字节:512 ns/次
MemProfileRate=1 让每次 64 字节分配从 10 ns 涨到 512 ns,约 50 倍;4096 只涨到 17 ns。想要更准的数字又不想拖慢程序,可以用中间值,但要在 main 开头设置、且只设一次(文档要求)。
pprof/pprof.go:125-130
// The heap profile reports statistics as of the most recently completed
// garbage collection; it elides more recent allocation to avoid skewing
// the profile away from live data and toward garbage.
// If there has been no garbage collection at all, the heap profile reports
// all known allocations. This exception helps mainly in programs running
// without garbage collection enabled, usually for debugging purposes.
mprof.go:856-870
// MemProfileRate controls the fraction of memory allocations
// that are recorded and reported in the memory profile.
// The profiler aims to sample an average of
// one allocation per MemProfileRate bytes allocated.
//
// To include every allocated block in the profile, set MemProfileRate to 1.
// To turn off profiling entirely, set MemProfileRate to 0.
//
// The tools that process the memory profiles assume that the
// profile rate is constant across the lifetime of the program
// and equal to the current value. Programs that change the
// memory profiling rate should do so just once, as early as
// possible in the execution of the program (for example,
// at the beginning of main).
var MemProfileRate int = 512 * 1024文档里的两处关键约束:profile 反映最近一次 GC;采样率必须在程序生命周期内保持不变。这两条都在本文的实验里得到了印证,但我没有测在运行中途改变采样率会导致什么错误结果。