Go 如何以及何时为有界队列通道分配内存?

问题描述

我正在使用 Go 的 pprof 工具来调查我的服务的内存使用情况。几乎所有内存使用都来自设置多个有界队列通道的单个函数。我对 pprof 在这里告诉我的内容有些困惑:

$ go tool pprof ~/pprof/pprof.server.alloc_objects.alloc_space.inuse_objects.inuse_space.007.pb.gz
File: server
Type: inuse_space
Time: Dec 21,2020 at 10:46am (PST)
Entering interactive mode (type "help" for commands,"o" for options)
(pprof) list foo
Total: 102.73MB
ROUTINE ======================== github.com/******/foo in ***.go
   79.10MB    79.10MB (flat,cum) 77.00% of Total
         .          .    135:
         .          .    136:func foo() {
         .          .    137:    
   14.04MB    14.04MB    138:    chanA := make(chan chanAEntry,bufferSize)
         .          .    139:    defer close(chanA)
         .          .    140:
         .          .    141:    
   19.50MB    19.50MB    142:    chanB := make(chan chanBCEntry,bufferSize)
         .          .    143:    defer close(chanB)
         .          .    144:
         .          .    145:    
   27.53MB    27.53MB    146:    chanC := make(chan chanBCEntry,bufferSize)
         .          .    147:    defer close(chanC)
         .          .    148:
         .          .    149:    
    7.92MB     7.92MB    150:    chanD := make(chan chanDEntry,bufferSize)
         .          .    151:    defer close(chanD)
         .          .    152:

看起来第 142 行负责 19.50MB 的分配,第 146 行负责 27.53MB,但这些行都在做同样的事情 - 它们创建具有相同输入类型和相同容量的缓冲通道。

  • 这是 pprof 进行随机抽样这一事实的产物吗?
  • Go 是否会懒惰地分配通道(fwiw,在让服务运行几天后,这些值最终会相等)?
  • pprof 是否报告了沿通道发送的对象所需的内存以及通道本身所需的内存?

解决方法

好的,我相信我已经想通了。看起来 Go 会急切地分配,这种差异只是由于 Go 内存分析器采样的方式造成的。

Go 急切地分配通道内存

makedocs 承诺

通道的缓冲区被初始化为指定的 缓冲容量。

我查看了 makechan 的代码,它在 make(chan chantype,size) 期间被调用。它总是直接调用 mallocgc - 没有懒惰。

查看 mallocgc 的代码,我们可以确认 mallocgc 中没有懒惰(除了文档注释没有提到懒惰,mallocgc 直接调用了 c.alloc)。

pprof 样本在堆分配级别,而不是调用函数级别

在查看 mallocgc 时,我找到了分析代码。在每个 mallocgc 调用中,Go 将检查是否满足其采样条件。如果是这样,它会调用 mProf_Malloc 将记录添加到堆配置文件中。我无法确认这是 pprof 使用的配置文件,但该文件中的注释表明它是。

采样条件基于自上次采样以来分配的字节数(平均而言,在每分配 runtime.MemProfileRate 个字节后,它从指数分布中抽取样本)。

这里的重要部分是对 mallocgc 的每次调用都有一定的采样概率,而不是对 foo 的每次调用。这意味着如果对 foo 的调用多次调用 mallocgc,我们预计只有部分 mallocgc 调用会被采样。

综合起来

每次我的函数 foo 运行时,它都会急切地为 4 个通道分配内存。在每次内存分配调用时,Go 都有可能记录堆配置文件。 Go 平均每 512kB 会记录一个堆配置文件(runtime.MemProfileRate 的默认值)。由于这些通道的总大小为 488kB,因此我们预计每次调用 foo 时平均只记录一次分配。我上面分享的配置文件是在服务重新启动后相对较快地获取的,因此分配的字节数差异是纯统计差异的结果。让服务运行一天后,配置文件稳定下来,显示第 142 行和第 146 行分配的字节数相等。