为何pprof的heap inuse_space远小于container_working_set_size指标
三个指标的核心含义
- pprof heap inuse_space:仅统计Go runtime当前正在使用的堆内存,也就是已分配且未被GC释放的用户态堆内存,不包含Go runtime本身占用的内存、栈内存、GC回收后未释放给操作系统的空闲堆内存,也不包含内核态分配给进程的内存。
- container_memory_rss:驻留内存,是当前进程实际占用的物理内存总和,包含Go堆内存、Go runtime元数据内存、goroutine栈内存、进程的代码段、数据段、共享库加载到物理内存的部分、透明大页占用、内核为进程分配的内核内存(比如socket缓冲区、页表等),也包含Go已经GC回收但还没归还给操作系统的空闲内存块。
- container_memory_working_set_bytes:工作集内存,是最近一段时间被进程访问过的内存总和,是cgroup统计的不会被系统优先回收的内存,等于总内存使用量减去最近未被访问的空闲文件缓存内存,一般会小于等于RSS(如果有部分RSS长时间没被访问会被统计为非工作集)。
差值产生的常见原因
- Go内存归还策略延迟:Go的GC回收了堆内存之后,不会立刻把空闲内存归还给操作系统,默认会保留一段时间以备后续分配使用,这部分内存不会被pprof统计为inuse_space,但会被计入RSS和工作集。可以通过
runtime.ReadMemStats的HeapReleased字段查看已经归还给操作系统的内存,HeapIdle减去HeapReleased就是Go持有但未被使用的空闲内存,这部分是差值的最常见来源。 - 非堆内存占用:Go程序的内存不止堆,还有goroutine栈(高并发场景下goroutine数量多的话栈内存累计会很大)、runtime的元数据(比如GC标记位图、mspan、mcache等结构)、cgo调用分配的内存、进程加载的动态链接库、代码段等,这些都不会被pprof的堆统计包含,但会被计入RSS。
- 内核分配的内存:进程运行过程中内核分配的内存,比如socket缓冲区、文件句柄对应的内核结构、页表项、共享内存段等,都会被计入容器的RSS和工作集,但完全不会被Go的pprof统计到。
- 透明大页(THP)的额外开销:如果开启了透明大页,内存会以2MB甚至1GB的大页为单位分配,会产生内存对齐的额外损耗,这部分损耗也会被计入RSS,不会被pprof统计。
- 统计时间差:Grafana的指标是定时采集的,pprof是瞬时抓取的,如果采集时间点不一致,也可能出现差值。
排查建议
可以先通过Go runtime内置的内存统计接口查看内存分布:
var stats runtime.MemStats runtime.ReadMemStats(&stats) // 打印HeapIdle、HeapReleased、StackInuse等字段对比差值
也可以直接在pprof交互模式执行top -inuse_space外的其他命令,查看空闲堆内存、栈内存等维度的占用情况,匹配差值来源。
内容的提问来源于stack exchange,提问作者zhuilong_gang
相关产品推荐
相关产品推荐

