这篇“golang pprof监控memory block mutex统计原理是什么”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“golang pprof监控memory block mutex统计原理是什么”文章吧。
bucket结构体介绍
// src/runtime/mprof.go:48type bucket struct {next *bucketallnext *buckettyp bucketType // memBucket or blockBucket (includes mutexProfile)hash uintptrsize uintptrnstk uintptr}
挨个详细解释下这个bucket结构体: 首先是两个指针,一个next 指针,一个allnext指针,allnext指针的作用就是形成一个链表结构,刚才提到的每次记录分配信息时,如果新增了bucket,那么这个bucket的allnext指针将会指向 bucket的链表头部。
bucket的链表头部信息是由一个全局变量存储起来的,代码如下:
// src/runtime/mprof.go:140var (mbuckets *bucket // memory profile bucketsbbuckets *bucket // blocking profile bucketsxbuckets *bucket // mutex profile bucketsbuckhash *[179999]*bucket
不同的指标类型拥有不同的链表头部变量,mbuckets 是内存指标的链表头,bbuckets 是block指标的链表头,xbuckets 是mutex指标的链表头。
这里还有个buckethash结构,无论那种指标类型,只要有bucket结构被创建,那么都将会在buckethash里存上一份,而buckethash用于解决hash冲突的方式则是将冲突的bucket通过指针形成链表联系起来,这个指针就是刚刚提到的next指针了。
至此,解释完了bucket的next指针,和allnext指针,我们再来看看bucket的其他属性。
// src/runtime/mprof.go:48type bucket struct {next *bucketallnext *buckettyp bucketType // memBucket or blockBucket (includes mutexProfile)hash uintptrsize uintptrnstk uintptr}
type 属性含义很明显了,代表了bucket属于那种指标类型。
hash 则是存储在buckethash结构内的hash值,也是在buckethash 数组中的索引值。
size 记录此次分配的大小,对于内存指标而言有这个值,其余指标类型这个值为0。
nstk 则是记录此次分配时,堆栈信息数组的大小。还记得在上一讲golang pprof监控系列(2) —— memory,block,mutex 使用里从网页看到的堆栈信息吗。
heap profile: 7: 5536 [110: 2178080] @ heap/10485762: 2304 [2: 2304] @ 0x100d7e0ec 0x100d7ea78 0x100d7f260 0x100d7f78c 0x100d811cc 0x100d817d4 0x100d7d6dc 0x100d7d5e4 0x100daba20#0x100d7e0ebruntime.allocm+0x8b/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1881#0x100d7ea77runtime.newm+0x37/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2207#0x100d7f25fruntime.startm+0x11f/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2491#0x100d7f78bruntime.wakep+0xab/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2590#0x100d811cbruntime.resetspinning+0x7b/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:3222#0x100d817d3runtime.schedule+0x2d3/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:3383#0x100d7d6dbruntime.mstart1+0xcb/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1419#0x100d7d5e3runtime.mstart0+0x73/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1367#0x100daba1fruntime.mstart+0xf/Users/lanpangzi/goproject/src/go/src/runtime/asm_arm64.s:117
nstk 就是记录的堆栈信息数组的大小,看到这里,你可能会疑惑,这里仅仅是记录了堆栈大小,堆栈的内容呢?关于分配信息的记录呢?
要回答这个问题,得搞清楚创建bucket结构体的时候,内存是如何分配的。
首先要明白结构体在进行内存分配的时候是一块连续的内存,例如刚才介绍bucket结构体的时候讲到的几个属性都是在一块连续的内存上,当然,指针指向的地址可以不和结构体内存连续,但是指针本身是存储在这一块连续内存上的。
接着,我们来看看runtime是如何创建一个bucket的。
// src/runtime/mprof.go:162func newBucket(typ bucketType, nstk int) *bucket {size := unsafe.Sizeof(bucket{}) + uintptr(nstk)*unsafe.Sizeof(uintptr(0))switch typ {default:throw("invalid profile bucket type")case memProfile:size += unsafe.Sizeof(memRecord{})case blockProfile, mutexProfile:size += unsafe.Sizeof(blockRecord{})}b := (*bucket)(persistentalloc(size, 0, &memstats.buckhash_sys))bucketmem += sizeb.typ = typb.nstk = uintptr(nstk)return b}
上述代码是创建一个bucket时源码, 其中persistentalloc 是runtime内部一个用于分配内存的方法,底层还是用的mmap,这里就不展开了,只需要知道该方法可以分配一段内存,size 则是需要分配的内存大小。
persistentalloc返回后的unsafe.Pointer可以强转为bucket类型的指针,unsafe.Pointer是go编译器允许的 代表指向任意类型的指针 类型。所以关键是看 分配一个bucket结构体的时候,这个size的内存空间是如何计算出来的。
首先unsafe.Sizeof 得到分配一个bucket代码结构 本身所需要的内存长度,然后加上了nstk 个uintptr 类型的内存长度 ,uintptr代表了一个指针类型,还记得刚刚提到nstk的作用吗?nstk表明了堆栈信息数组的大小,而数组中每个元素就是一个uintptr类型,指向了具体的堆栈位置。
接着判断 需要创建的bucket的类型,如果是memProfile 内存类型 则又用unsafe.Sizeof 得到一个memRecord的结构体所占用的空间大小,如果是blockProfile,或者是mutexProfile 则是在size上加上一个blockRecord结构体占用的空间大小。memRecord和blockRecord 里承载了此次内存分配或者此次阻塞行为的详细信息。
// src/runtime/mprof.go:59type memRecord struct {active memRecordCyclefuture [3]memRecordCycle}// src/runtime/mprof.go:120type memRecordCycle struct {allocs, frees uintptralloc_bytes, free_bytes uintptr}
关于内存分配的详细信息最后是有memRecordCycle 承载的,里面有此次内存分配的内存大小和分配的对象个数。那memRecord 里的active 和future又有什么含义呢,为啥不干脆用memRecordCycle结构体来表示此次内存分配的详细信息? 这里我先预留一个坑,放在下面在解释,现在你只需要知道,在分配一个内存bucket结构体的时候,也分配了一段内存空间用于记录关于内存分配的详细信息。
然后再看看blockRecord。
// src/runtime/mprof.go:135type blockRecord struct {count float64cycles int64}
blockRecord 就比较言简意赅,count代表了阻塞的次数,cycles则代表此次阻塞的周期时长,关于周期的解释可以看看我前面一篇文章golang pprof监控系列(2) —— memory,block,mutex 使用 ,简而言之,周期时长是cpu记录时长的一种方式。你可以把它理解成就是一段时间,不过时间单位不在是秒了,而是一个周期。
可以看到,在计算一个bucket占用的空间的时候,除了bucket结构体本身占用的空间,还预留了堆栈空间以及memRecord或者blockRecord 结构体占用的内存空间大小。
你可能会疑惑,这样子分配一个bucket结构体,那么如何取出bucket中的memRecord 或者blockRecord结构体呢? 答案是 通过计算memRecord在bucket 中的位置,然后强转unsafe.Pointer指针。
拿memRecord举例,
//src/runtime/mprof.go:187func (b *bucket) mp() *memRecord {if b.typ != memProfile {throw("bad use of bucket.mp")}data := add(unsafe.Pointer(b), unsafe.Sizeof(*b)+b.nstk*unsafe.Sizeof(uintptr(0)))return (*memRecord)(data)}
上面的地址可以翻译成如下公式:
memRecord开始的地址 = bucket指针的地址 + bucket结构体的内存占用长度 + 栈数组占用长度
这一公式成立的前提便是 分配结构体的时候,是连续的分配了一块内存,所以我们当然能通过bucket首部地址以及中间的空间长度计算出memRecord开始的地址。
至此,bucket的结构体描述算是介绍完了,但是还没有深入到记录指标信息的细节,下面我们深入研究下记录细节,正戏开始。
记录指标细节介绍
由于内存分配的采样还是和block阻塞信息的采样有点点不同,所以我还是决定分两部分来介绍下,先来看看内存分配时,是如何记录此次内存分配信息的。
memory
首先在上篇文章golang pprof监控系列(2) —— memory,block,mutex 使用 我介绍过 MemProfileRate ,MemProfileRate 用于控制内存分配的采样频率,代表平均每分配MemProfileRate字节便会记录一次内存分配记录。
当触发记录条件时,runtime便会调用 mProf_Malloc 对此次内存分配进行记录,
// src/runtime/mprof.go:340func mProf_Malloc(p unsafe.Pointer, size uintptr) {var stk [maxStack]uintptrnstk := callers(4, stk[:])lock(&proflock)b := stkbucket(memProfile, size, stk[:nstk], true)c := mProf.cyclemp := b.mp()mpc := &mp.future[(c+2)%uint32(len(mp.future))]mpc.allocs++mpc.alloc_bytes += sizeunlock(&proflock)systemstack(func() {setprofilebucket(p, b)})}
实际记录之前还会先获取堆栈信息,上述代码中stk 则是记录堆栈的数组,然后通过 stkbucket 去获取此次分配的bucket,stkbucket 里会判断是否先前存在一个相同bucket,如果存在则直接返回。而判断是否存在相同bucket则是看存量的bucket的分配的内存大小和堆栈位置是否和当前一致。
// src/runtime/mprof.go:229for b := buckhash[i]; b != nil; b = b.next {if b.typ == typ && b.hash == h && b.size == size && eqslice(b.stk(), stk) {return b}}
通过刚刚介绍bucket结构体,可以知道 buckhash 里容纳了程序中所有的bucket,通过一段逻辑算出在bucket的索引值,也就是i的值,然后取出buckhash对应索引的链表,循环查找是否有相同bucket。相同则直接返回,不再创建新bucket。
让我们再回到记录内存分配的主逻辑,stkbucket 方法创建或者获取 一个bucket之后,会通过mp()方法获取到其内部的memRecord结构,然后将此次的内存分配的字节累加到memRecord结构中。
不过这里并不是直接由memRecord 承载累加任务,而是memRecord的memRecordCycle 结构体。
c := mProf.cyclemp := b.mp()mpc := &mp.future[(c+2)%uint32(len(mp.future))]mpc.allocs++mpc.alloc_bytes += size
这里先是从memRecord 结构体的future结构中取出一个memRecordCycle,然后在memRecordCycle上进行累加字节数,累加分配次数。
这里有必要介绍下mProf.cycle 和memRecord中的active和future的作用了。
我们知道内存分配是一个持续性的过程,内存的回收是由gc定时执行的,golang设计者认为,如果每次产生内存分配的行为就记录一次内存分配信息,那么很有可能这次分配的内存虽然程序已经没有在引用了,但是由于还没有垃圾回收,所以会造成内存分配的曲线就会出现严重的倾斜(因为内存只有垃圾回收以后才会被记录为释放,也就是memRecordCycle中的free_bytes 才会增加,所以内存分配曲线会在gc前不断增大,gc后出现陡降)。
所以,在记录内存分配信息的时候,是将当前的内存分配信息经过一轮gc后才记录下来,mProf.cycle 则是当前gc的周期数,每次gc时会加1,在记录内存分配时,将当前周期数加2与future取模后的索引值记录到future ,而在释放内存时,则将 当前周期数加1与future取模后的索引值记录到future,想想这里为啥要加1才能取到 对应的memRecordCycle呢? 因为当前的周期数比起内存分配的周期数已经加1了,所以释放时只加1就好。
// src/runtime/mprof.go:362func mProf_Free(b *bucket, size uintptr) {lock(&proflock)c := mProf.cyclemp := b.mp()mpc := &mp.future[(c+1)%uint32(len(mp.future))]mpc.frees++mpc.free_bytes += sizeunlock(&proflock)}
在记录内存分配时,只会往future数组里记录,那读取内存分配信息的 数据时,怎么读取呢?
还记得memRecord 里有一个类型为memRecordCycle 的active属性吗,在读取的时候,runtime会调用 mProf_FlushLocked()方法,将当前周期的future数据读取到active里。
// src/runtime/mprof.go:59type memRecord struct {active memRecordCyclefuture [3]memRecordCycle}// src/runtime/mprof.go:120type memRecordCycle struct {allocs, frees uintptralloc_bytes, free_bytes uintptr}// src/runtime/mprof.go:305func mProf_FlushLocked() {c := mProf.cyclefor b := mbuckets; b != nil; b = b.allnext {mp := b.mp()// Flush cycle C into the published profile and clear// it for reuse.mpc := &mp.future[c%uint32(len(mp.future))]mp.active.add(mpc)*mpc = memRecordCycle{}}}
代码比较容易理解,mProf.cycle获取到了当前gc周期,然后用当前周期从future里取出 当前gc周期的内存分配信息 赋值给acitve ,对每个内存bucket都进行这样的赋值。
赋值完后,后续读取当前内存分配信息时就只读active里的数据了,至此,算是讲完了runtime是如何对内存指标进行统计的。
接着,我们来看看如何对block和mutex指标进行统计的。
block mutex
block和mutex的统计是由同一个方法,saveblockevent 进行记录的,不过方法内部针对这两种类型还是做了一点点不同的处理。
有必要注意再提一下,mutex是在解锁unlock时才会记录一次阻塞行为,而block在记录mutex锁阻塞信息时,是在开始执行lock调用的时候记录的 ,除此以外,block在select 阻塞,channel通道阻塞,wait group 产生阻塞时也会记录一次阻塞行为。
// src/runtime/mprof.go:417func saveblockevent(cycles, rate int64, skip int, which bucketType) {gp := getg()var nstk intvar stk [maxStack]uintptrif gp.m.curg == nil || gp.m.curg == gp {nstk = callers(skip, stk[:])} else {nstk = gcallers(gp.m.curg, skip, stk[:])}lock(&proflock)b := stkbucket(which, 0, stk[:nstk], true)if which == blockProfile && cycles < rate {// Remove sampling bias, see discussion on http://golang.org/cl/299991.b.bp().count += float64(rate) / float64(cycles)b.bp().cycles += rate} else {b.bp().count++b.bp().cycles += cycles}unlock(&proflock)}
首先还是获取堆栈信息,然后stkbucket() 方法获取到 一个bucket结构体,然后bp()方法获取了bucket里的blockRecord 结构,并对其count次数和cycles阻塞周期时长进行累加。
// src/runtime/mprof.go:135type blockRecord struct {count float64cycles int64}
注意针对blockProfile 类型的次数累加 还进行了特别的处理,还记得上一篇golang pprof监控系列(2) —— memory,block,mutex 使用提到的BlockProfileRate参数吗,它是用来设置block采样的纳秒采样率的,如果阻塞周期时长cycles小于BlockProfileRate的话,则需要fastrand函数乘以设置的纳秒时间BlockProfileRate 来决定是否采样了,所以如果是小于BlockProfileRate 并且saveblockevent进行了记录阻塞信息的话,说明我们只是采样了部分这样情况的阻塞,所以次数用BlockProfileRate 除以 此次阻塞周期时长数,得到一个估算的总的 这类阻塞的次数。
读取阻塞信息就很简单了,直接读取阻塞bucket的count和周期数即可。
以上就是关于“golang pprof监控memory block mutex统计原理是什么”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注编程网行业资讯频道。