开发者

golang pprof监控memory block mutex统计原理分析

开发者 https://www.devze.com 2023-04-07 11:19 出处:网络 作者: 蓝胖子的编程梦
目录引言bucket结构体介绍记录指标细节介绍memoryblock mutex总结引言 在上一篇文章 golang pprof监控系列(2) —— memory,block,mutex 使用里我讲解了这3种性能指标如何在程序中暴露以及各自监控的范
目录
  • 引言
  • bucket结构体介绍
  • 记录指标细节介绍
    • memory
    • block mutex
  • 总结

    引言

    在上一篇文章 golang pprof监控系列(2) —— memory,block,mutex 使用里我讲解了这3种性能指标如何在程序中暴露以及各自监控的范围。也有提到memory,block,mutex 把这3类数据放在一起讲,是因为他们统计的原理是很类似的。今天来看看它们究竟是如何统计的。

    先说下结论,这3种类型在runtime内部都是通过一个叫做bucket的结构体做的统计,bucket结构体内部有指针指向下一个bucket 这样构成了bucket的链表,每次分配内存,或者每次阻塞产生时,会判断是否会创建一个新的bucket来记录此次分配信息。

    先来看下bucket里面有哪些信息。

    bucket结构体介绍

    // src/runtime/mprof.go:48
    type bucket struct {
    	next    *bucket
    	allnext *bucket
    	typ     bucketType // memBucket or blockBucket (includes mutexProfile)
    	hash    uintptr
    	size    uintptr
    	nstk    uintptr
    }
    

    挨个详细解释下这个bucket结构体: 首先是两个指针,一个next 指针,一个allnext指针,allnext指针的作用就是形成一个链表结构,刚才提到的每次记录分配信息时,如果新增了bucket,那么这个bucket的allnext指针将会指向 bucket的链表头部。

    bucket的链表头部信息是由一个全局变量存储起来的,代码如下:

    // src/runtime/mprof.go:140
    var (
    	mbuckets  *bucket // memory profile buckets
    	bbuckets  *bucket // blocking profile buckets
    	xbuckets  *bucket // mutex profile buckets
    	buckhash  *[179999]*bucket
    

    不同的指标类型拥有不同的链表头部变量,mbuckets 是内存指标的链表头,bbuckets 是block指标的链表头,xbuckets 是mutex指标的链表头。

    这里还有个buckethash结构,无论那种指标类型,只要有bucket结构被创建,那么都将会在buckethash里存上一份,而buckethash用于解决hash冲突的方式则是将冲突的bucket通过指针形成链表联系起来,这个指针就是刚刚提到的next指针了。

    至此,解释完了bucket的next指针,和allnext指针,我们再来看看bucket的其他属性。

    // src/runtime/mprof.go:48
    type bucket struct {
    	next    *bucket
    	allnext *bucket
    	typ     bucketType // memBucket or blockBucket (includes mutexProfile)
    	hash    uintptr
    	size    uintptr
    	nstk    uintptr
    }
    

    type 属性含义很明显了,代表了bucket属于那种指标类型。

    hash 则是存储在buckethash结构内的hash值,也是在buckethash 数组中的索引值。

    size 记录此次分配的大小,对于内存指标而言有这个值,其余指标类型这个值为0。

    nstk 则是记录此次分配时,堆栈信息数组的大小。还记得在上一讲golang pprof监控系列(2) —— memory,block,mutex 使用里从网页看到的堆栈信息吗。

    heap profile: 7: 5536 [110: 2178080] @ heap/1048576
    2: 2304 [2: 2304] @ 0x100d7e0ec 0x100d7ea78 0x100d7f260 0x100d7f78c 0x100d811cc 0x100d817d4 0x100d7d6dc 0x100d7d5e4 0x100daba20
    #	0x100d7e0eb	runtime.allocm+0x8b		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1881
    #	0x100d7ea77	runtime.newm+0x37		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2207
    #	0x100d7f25f	runtime.startm+0x11f		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2491
    #	0x100d7f78b	runtime.wakep+0xab		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:2590
    #	0x100d811cb	runtime.resetspinning+0x7b	/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:3222
    #	0x100d817d3	runtime.schedule+0x2d3		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:3383
    #	0x100d7d6db	runtime.mstart1+0xcb		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1419
    #	0x100d7d5e3	runtime.mstart0+0x73		/Users/lanpangzi/goproject/src/go/src/runtime/proc.go:1367
    #	0x100daba1f	runtime.mstart+0xf		/Users/lanpangzi/goproject/src/go/src/runtime/asm_arm64.s:117
    

    nstk 就是记录的堆栈信息数组的大小,看到这里,你可能会疑惑,这里仅仅是记录了堆栈大小,堆栈的内容呢?关于分配信息的记录呢?

    要回答这个问题,得搞清楚创建bucket结构体的时候,内存是如何分配的。

    首先要明白结构体在进行内存分配的时候是一块连续的内存,例如刚才介绍bucket结构体的时候讲到的几个属性都是在一块连续的内存上,当然,指针指向的地址可以不和结构体内存连续,但是指针本身是存储在这一块连续内存上的。

    接着,我们来看看runtime是如何创建一个bucket的。

    // src/runtime/mprof.go:162
    func 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 += size
    	b.typ = typ
    	b.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:59
    type memRecord struct {
    	active memRecordCycle
    	future [3]memRecordCycle
    }
    // src/runtime/mprof.go:120
    type memRecordCycle struct {
    	allocs, frees           uintptr
    	alloc_bytes, free_bytes uintptr
    }
    

    关于内存分配的详细信息最后是有memRecordCycle 承载的,里面有此次内存分配的内存大小和分配的对象个数。那memRecord 里的active 和future又有什么含义呢,为啥不干脆用memRecordCycle结构体来表示此次内存分配的详细信息? 这里我先预留一个坑,放在下面在解释,现在你只需要知道,在分配一个内存bucket结构体的时候,也分配了一段内存空间用于记录关于内存分配的详细信息。

    然后再看看blockRecord。

    // src/runtime/mprof.go:135
    type blockRecord struct {
    	count  float64
    	cycles 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:187
    func (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:340
    func mProf_Malloc(p unsafe.Pointer, size uintptr) {
    	var stk [maxStack]uintptr
    	nstk := callers(4, stk[:])
    	lock(&proflock)
    	b := stkbucket(memProfile, size, stk[:nstk], true)
    	c := mProf.cycle
    	mp := b.mp()
    	mpc := &mp.future[(c+2)%uint32(len(mp.future))]
    	mpc.allocs++
    	mpc.alloc_bytes += size
    	unlock(&proflock)
    	systemstack(func() {
    		setprofilebucket(p, b)
    	})
    }
    

    实际记录之前还会先获取堆栈信息,上述代码中stk 则是记录堆栈的数组,然后通过 stkbucket 去获取此次分配的bucket,stkbucket 里会判断是否先前存在一个相同bucket,如果存在则直接返回。而判断是否存在相同bucket则是看存量的bucket的分配的内存大小和堆栈位置是否和当前一致。

    // src/runtime/mprof.go:229
    for 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.cycle
    	mp := 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:362
    func mProf_Free(b *bucket, size uintptr) {
    	lock(&proflock)
    	c := mProf.cycle
    	mp := b.mp()
    	mpc := &mp.future[(c+1)%uint32(len(mp.future))]
    	mpc.frees++
    	mpc.free_bytes += size
    	unlock(&proflock)
    }
    

    在记录内存分配时,只会往future数组里记录,那读取内存分配信息的 数据时,怎么读取呢?

    还记得memRecord 里有一个类型为memRecordCycle 的active属性吗,在读取的时候,runtime会调用 mProf_FlushLocked()方法,将php当前周期的future数据读取到active里。

    // src/runtime/mprof.go:59
    type memRecord struct {
    	active memRecordCycle
    	future [3]memRecordCycle
    }
    // s编程rc/runtime/mprof.go:120
    type memRecordCycle struct {
    	allocs, frees           uintptr
    	alloc_bytes, free_bytes uintptr
    }
    // src/runtime/mprof.go:305
    func mProf_FlushLocked() {
    	c := mProf.cycle
    	for 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里开发者_Python的数据了,至此,http://www.devze.com算是讲完了runtime是如何对内存指标进行统计的。

    接着,我们来看看如何对block和mutex指标进行统计的。

    block mutex

    block和mutex的统计是由同一个方法,saveblockevent 进行记录的,不过方法内部针对这两种类型还是做了一点点不同的处理。

    有必要注意再提一下,mutex是在解锁unlock时才会记录一次阻塞行为,而block在记录mutex锁阻塞信息时,是在开始执行lock调用的时候记录的 ,除此以外,block在select 阻塞,channel通道阻塞,wait group 产生阻塞时也会记录一次阻塞行为。

    // src/runtime/mprof.go:417
    func saveblockevent(cycles, rate int64, skip int, which bucketType) {
    	gp := getg()
    	var nstk int
    	var stk [maxStack]uintptr
    	if 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:135
    type blockRecord struct {
    	count  float64
    	cycles int64
    }
    

    注意针对blockProfile 类型的次数累加 还进行了特别的处理,还记得上一篇golang pprof监控系列(2) —— memory,block,mutex 使用提到的BlockProfileRate参数吗,它是用来设置block采样的纳秒采样率的,如果阻塞周期时长cycles小于BlockProfileRate的话,则需要fastrand函数乘以设置的纳秒时间BlockProfileRate 来决定是否采样了,所以如果是小于BlockProfileRate 并且saveblockevent进行了记录阻塞信息的话,说明我们只是采样了部分这样情况的阻塞,所以次数用BlockProfileRate 除以 此次阻塞周期时长数,得到一个估算的总的 这类阻塞的次数。

    读取阻塞信息就很简单了,直接读取阻塞bucket的count和周期数即可。

    总结

    至此,算是介绍完了这3种指标类型的统计原理,简而言之,就是通过一个携带有堆栈信息的bucket对每次内存分配或者阻塞行为进行采样记录,读取内存分配信息 或者阻塞指标信息的 时候便是所有的bjsucket信息读取出来。

    以上就是golang pprof监控memory block mutex统计原理分析的详细内容,更多关于golang pprof监控统计的资料请关注我们其它相关文章!

    0

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    关注公众号