pprof 入门
资料链接: pprof blog
在阅读了这篇博客之后,可以算是勉强入门了pprof工具。
主要在用pprof时,主要是用来分析以下这4种内容:
- CPU Profiling:CPU 分析,按照一定的频率采集所监听的应用程序 CPU(含寄存器)的使用情况,可确定应用程序在主动消耗 CPU 周期时花费时间的位置
- Memory Profiling:内存分析,在应用程序进行堆分配时记录堆栈跟踪,用于监视当前和历史内存使用情况,以及检查内存泄漏
- Block Profiling:阻塞分析,记录 goroutine 阻塞等待同步(包括定时器通道)的位置
- Mutex Profiling:互斥锁分析,报告互斥锁的竞争情况
博客中的大部分例子都挺简单的,不过其中关于内存问题排查的例子引起了我的兴趣,接下来就着这个例子让我来展开分析。
排查内存占用
在博客里给的内存占用过高的例子中,我们可以看见这样一张图:
根据博客所写内容:
可以看到这次出问题的地方在
github.com/wolfogre/go-pprof-practice/animal/muridae/mouse.(*Mouse).Steal,函数内容如下:
func (m *Mouse) Steal() {
log.Println(m.Name(), "steal")
max := constant.Gi
for len(m.buffer) * constant.Mi < max {
m.buffer = append(m.buffer, [constant.Mi]byte{})
}
}
可以看到,这里有个循环会一直向 m.buffer 里追加长度为 1 MiB 的数组,直到总容量到达 1 GiB 为止,且一直不释放这些内存,这就难怪会有这么高的内存占用了。
也就是说内存分配应该到1GB就为止了,为什么通过pprof分析的却是占用了1.5GB的内存呢?
内存超分配可能情况
Slice overhead
- Go 中的每个切片都有一些与之相关的附加元数据,如长度、容量等。此开销与分配的切片数量成正比,而不是与数据本身的大小成正比。 因此,分配大量较小的切片可能会产生不小的开销。
- 当把数据附加到切片时,Go 通常会分配比请求更多的容量,以避免随着切片的增长而频繁重新分配。 默认情况下,每次需要增长时,容量都会增加一倍。 因此,即使您一次只附加 1MB,容量也可能是 2MB、4MB 等。
Fregmentation
Go 运行时内存分配器会去尝试满足现有空闲块的分配请求,以避免堆不必要的增长。但随着时间的推移,可能会出现碎片,导致许多较小的空闲块无法用于较大的分配。这导致分配的内存比理论上需要的多。
Page alignment
运行时将内存分配与页面大小块(可能是 4K 左右)对齐。任何未使用的页面内存仍然计入总数。
可能会有人问了,为什么会需要页面对其呢,让我来简要的讲几点原因:
- 分页和加载的效率:操作系统以称为页(通常为 4KB)的块来分配和管理内存。通过将分配与页面大小对齐,操作系统可以在调入和调出程序数据时更有效地将程序数据加载到内存中。
- 避免碎片:如果分配不是页面对齐的,则可能会在虚拟地址空间中产生大量碎片,留下的间隙太小而无法使用。与页面对齐可以减少这种碎片。
- 硬件要求:某些 CPU 架构需要某些数据类型(例如整数和指针)在内存中对齐以获得最佳性能。即使分配器没有完美地打包数据,将分配与页面对齐也可以确保这一点。
- 并发性:对齐内存可以防止并发程序中出现一些错误共享问题。跨页边界的数据更有可能导致 CPU 核心之间的缓存争用。
- 操作系统元数据:操作系统存储有关每个内存页的元数据。通过对齐分配,每个页面的元数据可以更好地排列。
其他开销
调度、垃圾收集、线程等也会产生开销。因此,Go 程序使用的总内存通常大于实时数据的总和。
总结
由于我们可以看mouse.go中:
type Mouse struct {
buffer [][constant.Mi]byte
slowBuffer [][constant.Mi]byte
}
由于 m.buffer 是字节切片的切片(即 [][]byte),因此附加的每个 1MB 块实际上是一个单独的切片,会带来一些开销。
在 64 位系统上,每个切片的开销约为 24 字节 - 它包括指向底层数组的指针(8 字节)、长度(8 字节)和容量(8 字节)。
因此,如果通过附加 1000 x 1MB 切片来分配 1GB,则 1000 x 24 字节 = 24KB 切片开销。
再加上每个附加、碎片、页面对齐等预先分配的任何未使用的容量。
因此 1.5GB 的总量是有可能达到的 -> 1GB 的实际数据,加上数百 KB 的切片开销和其他超分配容量还有碎片以及页面对其导致的开销。
那么该怎么样减少这样的开销呢?凭个人经验,有以下几点方法:
- 预分配一个大的 []byte 缓冲区,而不是附加许多较小的切片
- 使用 bytes.Buffer 可以有效地进行大小调整并且使每个元素的开销更少
- 将多个append操作一起进行批处理,变成成更大的块以减少切片数量