我正在使用 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:
Run Code Online (Sandbox Code Playgroud)
看起来第 142 行负责 19.50MB 的分配,第 146 行负责 27.53MB,但这些行正在做同样的事情 - 它们创建具有相同输入类型和相同容量的缓冲通道。
好吧,我相信我已经弄清楚了。看起来 Go 会急切地分配,而差异只是由于 Go 内存分析器采样的方式造成的。
Go 急切地分配通道内存
文档承诺make
通道的缓冲区使用指定的缓冲区容量进行初始化。
我研究了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 的默认值)。foo由于这些通道的总大小为 488kB,因此平均而言,我们预计每次调用时仅记录一次分配。我上面分享的配置文件是在服务重新启动后相对较短时间内获取的,因此分配字节数的差异是纯粹统计方差的结果。让服务运行一天后,配置文件稳定下来,显示第 142 行和第 146 行分配的字节数相等。
| 归档时间: |
|
| 查看次数: |
1094 次 |
| 最近记录: |