爱上开源之golang入门至实战第三章-内存Alloc分析
Memory Allocs
Allocs也是关注与内存方面的数据采样,而且由于Allocs的采样数据和Heap的采样数据大致上都是一样的。
所以很多golang的开发人员非常容易忽略这个Allocs的数据采样
上面就是Allocs的数据采样的文本格式的内容; 和Heap的对照一下,确实发现是一致的; 我们可以详细查看pprof输出allocs的源代码runtime/pprof/pprof.go
line 546 行函数writeHeapInternal
的实现。可以发现Heap和Allocs两者都是使用同一个函数进行输出的;在同一个时间点的采样上,输出的数据就是一致的。
虽然Heap和Allocs两者的数据样本一致;但是在pprof进行分析的时候,两者所主要针对的方向是完全不同的方向; 如同pprof的index页面里描述的一般:
我们来看看
allocs: A sampling of all past memory allocations.
heap: A sampling of memory allocations of live objects.
Heap分析的最要采样对象是采样时间点时的活动对象(未销毁)的数据样本;而Allocs分析的主要采用对象是至采用时间点过去时间里的内存分配的数据样本;这个内存分配的样本对于的内存有可能已经销毁了,也有可能仍然存在;Allocs更多的关注的是内存申请和分配的过程;Heap关注的内存申请和分配的结果;
上一个章节里;我们讲解了Heap的数据分析过程;通过Heap的数据分析;我们可以有依据的对内存不释放或者内存溢出的可能性的地方进行判断和分析; 是个非常不错的内存诊断的方法; 这个Allocs的数据分析;又能给我们带来什么样的性能和内存分析方法,我们就一起来看看吧。
Allocs的手工埋点
虽然如上所述; Allocs和Heap的输出的代码是完全同样的一套代码,但是在我们对Allocs进行埋点的时候,可以完全使用和上一节一模一样的调用方式; 两者输出一模一样; 在需要采样的时间点上执行,下面代码即可:
if err := pprof.WriteHeapProfile(f1); err != nil {
panic("could not start heap-1 profile: ")
}
上面的这个方法虽然能够到达同样的效果,但是是阅读了pprof的源码后;发现输出是一致的;这样情况下的偷巧的方法;如果以后golang的pprof升级,就可能有问题了;这里推荐使用标准allocs埋点;
allocProfile := pprof.Lookup("allocs")
f3, err := os.Create("alloc-prof-3")
if err != nil {
panic(err)
}
defer f3.Close()
if err := allocProfile.WriteTo(f3, 0); err != nil {
panic("could not start alloc-3 profile: ")
}
网上的相关资料非常的少; 上述有关allocs的手工埋点的方法;是阅读pprof的相关源代码以后;经过测试是完全可行的方法;
pprof工具Allocs分析
运行命令
go tool pprof http://localhost:8999/debug/pprof/allocs
图中是直接从本地埋点产生的alloc-prof-3这个采样文件里直接进行本地分析的; 和go tool pprof http://localhost:8999/debug/pprof/allocs
的方法没什么差别,只是数据源来源不一样而已; 下面所讲到的分析方法和过程,均不受此影响
Top命令
Top命令的结果,列表出采集到的发生了内存分配的函数调用;
其中每一行都表示一个采集到的函数调用,
每列都对应着采集到的数据分析; 列的参照如下
flat:函数在调用中内存分配的数量
flat%:函数在调用中内存分配的百分比
sum%:所有函数累加使用内存分配的比例,即所有flat%的总和
cum: 函数以及子函数运行所使用内存分配,应该大于等于flat
cum%: 函数以及子函数运行所使用内存分配的比例,应该大于等于flat%
函数的名字
这里可以看到调用现在内存分配最多是分配了1.07G, 占所有内存分配的99.54%,函数go-in-practice/code/charpter-01.TestAllocPprof
, 这个函数的调用占用整个程序运行过程中所有的的内存分配, 这个函数究竟做了哪些动作,导致内存分配如此之高, 我们可以在pprof里运行list命令, 进一步进行分析
使用pprof命令:list 函数名。 可以用pprof分析函数中的哪一行导致的内存占用
(pprof) list TestAllocPprof
Total: 1.07GB
ROUTINE ======================== go-in-practice/code/charpter-01.TestAllocPprof in E:\WORK\PROJECT\git\go\go-in-practice\code\charpter-01\lesson02_test.go
. . 183: for i := 0; i < poolSize; i++ {
99.47MB 99.47MB 184: pool = make([]byte, poolSize)
. . 185: sum += poolSize
. . 186: pool[0] = byte('c')
. . 187: }
. . 188:
. 2.09MB 189: if err := allocProfile.WriteTo(f2, 0); err != nil {
. . 190: panic("could not start alloc-2 profile: ")
. . 191: }
. . 192:
. . 193: var pool2 []byte
. . 194: for i := 0; i < poolSize/10; i++ {
993.44MB 993.44MB 195: pool2 = make([]byte, poolSize*100)
. . 196: sum += poolSize * 10
. . 197: pool2[0] = byte('c')
. . 198: }
. . 199:
. . 200: if err := allocProfile.WriteTo(f3, 0); err != nil {
通过这个命令,可以直接追击到源代码的行数,从而进行仔细的调用过程分析
代码195行:
pool2 = make([]byte, poolSize*100)
make([]byte) 进行了内存是申请的操作, 在外部的for循环里,循环的进行申请,申请的数量达到了1G左右。
使用图形化web命令进行分析
在pprof的交互模式里输入web命令
使用tree命令查看调用关系
(pprof) tree
Showing nodes accounting for 1.07GB, 99.54% of 1.07GB total
Dropped 24 nodes (cum <= 0.01GB)
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
1.07GB 100% | testing.tRunner
1.07GB 99.54% 99.54% 1.07GB 99.73% | go-in-practice/code/charpter-01.TestAllocPprof
----------------------------------------------------------+-------------
0 0% 99.54% 1.07GB 99.73% | testing.tRunner
1.07GB 100% | go-in-practice/code/charpter-01.TestAllocPprof
----------------------------------------------------------+-------------
使用traces命令查看采样数据
(pprof) traces
Type: alloc_space
Time: Jul 5, 2022 at 9:08am (CST)
-----------+-------------------------------------------------------
bytes: 1000kB
993.44MB go-in-practice/code/charpter-01.TestAllocPprof
testing.tRunner
-----------+-------------------------------------------------------
bytes: 256kB
650.62kB compress/flate.(*compressor).init
compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.writeHeapProto
runtime/pprof.writeHeapInternal
runtime/pprof.writeAlloc
runtime/pprof.(*Profile).WriteTo
go-in-practice/code/charpter-01.TestAllocPprof
testing.tRunner
-----------+-------------------------------------------------------
bytes: 136kB
583.01kB compress/flate.newDeflateFast
compress/flate.(*compressor).init
compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.writeHeapProto
runtime/pprof.writeHeapInternal
runtime/pprof.writeAlloc
runtime/pprof.(*Profile).WriteTo
go-in-practice/code/charpter-01.TestAllocPprof
testing.tRunner
-----------+-------------------------------------------------------
bytes: 648kB
902.59kB compress/flate.NewWriter
compress/gzip.(*Writer).Write
runtime/pprof.(*profileBuilder).build
runtime/pprof.writeHeapProto
runtime/pprof.writeHeapInternal
runtime/pprof.writeAlloc
runtime/pprof.(*Profile).WriteTo
go-in-practice/code/charpter-01.TestAllocPprof
testing.tRunner
-----------+-------------------------------------------------------
bytes: 1kB
1MB runtime.allocm
runtime.newm
runtime.startm
runtime.wakep
runtime.resetspinning
runtime.schedule
runtime.park_m
runtime.mcall
-----------+-------------------------------------------------------
bytes: 10kB
99.47MB go-in-practice/code/charpter-01.TestAllocPprof
testing.tRunner
-----------+-------------------------------------------------------
runtime.schedule
runtime.mstart1
runtime.mstart0
runtime.mstart
-----------+-------------------------------------------------------
bytes: 416B
1MB runtime.malg
runtime.newproc1
runtime.newproc.func1
runtime.systemstack
-----------+-------------------------------------------------------
可以看到这里内存分配了1G;但是活动的内存对象只有10K和1000K; 在这样的情况下,我们可以看到,以上场景的时候;每一次修改pool对象的时候,都重新的进行了内存申请;我们可以通过建立对象池的方式,来降低内存申请数量; 对当前这样场景的进行优化;在fasthttp项目中; 就大量使用了池化的实现, 来对一些操作频繁的对象类型进行了池化; 每次需要构建一个新对象时,在对象池里进行查询,如果有没有使用到的对象,就拿到空闲对象,然后对空闲对象进行初始化,从而不需要重新去对对象进行结构体的内存申请;通过这样的优化方式;fasthttp在大并发的情况下,依然能够保证非常高的响应要求;当然;时间和空间在优化的过程中,往往是对矛盾体;比如我们这里提到的Heap和Allocs; fasthttp的这种方式; 降低了Allocs的次数;但是就会提高Heap的大小; 如果非频繁调用, 这两者之间的差异不会很明显; 但是如果频繁调用的话, 优化前后的差异一定是越来越明显。
Allocs的pprof样本中的数据项
通过http://localhost:8999/debug/pprof/可以查看allocs的信息
技巧
这些信息是样本中的数据项,相对于我们通过web访问时,访问当前时间的heap的一个快照; 熟悉这些数据项所表示的含义,也非常对我们了解当前程序运行时的内存情况非常有帮助。
这里对应的样本的输出的源代码可以参考net/http/pprof/pprof.go
, runtime/pprof/pprof.go
和runtime/mstats.go