bigywater.tech
Go · 67

heap profile:inuse 与 alloc 的区别,采样率怎么影响精度

heap profile 有 inuse 和 alloc 两种视角,内存泄漏和分配热点要看不同的列;它还是按采样得到的估计值,而且只反映最近一次 GC 时的状态。这篇用一个已知的程序验证这几点,并量采样率的精度与代价。

98.10 MB
garbageX 在 alloc_space 里的占比最大,但它不在 inuse_space 的前列
20133 vs 54477 kB
不 GC 与 GC 之后再写 profile,retainY 的 inuse_space
10 → 512 ns
分配 64 字节在默认采样率与 MemProfileRate=1 下的耗时

inuse 和 alloc

实测 · 本机执行输出
  程序: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).sleep

inuse_space 只算还活着的,alloc_space 算累计分配。丢弃的 98 MB 垃圾在后者里排第一、在前者里不出现。排查泄漏看 inuse,排查 GC 压力看 alloc。

profile 是 GC 时的快照

实测 · 本机执行输出
  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;采样率必须在程序生命周期内保持不变。这两条都在本文的实验里得到了印证,但我没有测在运行中途改变采样率会导致什么错误结果。

分步演示

heap profile
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。小对象实验里 retainZ 包含 keepZ 切片本身的底层数组(每个 []byte 头 24 字节),所以基准值是 14200 kB 而不是 10000 kB;MemProfileRate=1 的结果当作基准;每个采样率各运行 6 次(进程重启);分配耗时用 testing.Benchmark,单次测量;读过的源码:runtime/pprof/pprof.go 的 heap profile 说明,runtime/mprof.go 的 MemProfileRate
    • 没有验证:Linux 上的结果、go tool pprof 网页界面、allocs profile 与 heap profile 的区别细节、运行中途修改采样率的后果、inuse_objects 与 alloc_objects 的对照、真实服务中 /debug/pprof/heap 的开销
    京ICP备2026067502号-1京公网安备11010502063487号