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

目录
  • 引言
  • 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()方法,将当前周期的future数据读取到active里。

// 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
}
// 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里的数据了,至此,算是讲完了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对每次内存分配或者阻塞行为进行采样记录,读取内存分配信息 或者阻塞指标信息的 时候便是所有的bucket信息读取出来。

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

(0)

相关推荐

  • golang pprof监控memory block mutex使用指南

    目录 profile trace 网页显示 如何使用 http 接口暴露的方式 allocs ,heap block mutex 代码生成profile文件的方式 总结 profile profile的中文被翻译轮廓,对于计算机程序而言,抛开业务逻辑不谈,它的轮廓是是啥呢?不就是cpu,内存,各种阻塞开销,线程,协程概况 这些运行指标或环境.golang语言自带了工具库来帮助我们描述,探测,分析这些指标或者环境信息,让我们来学习它. 在上一篇golang pprof 监控系列(1) —— go

  • Golang pprof性能测试与分析讲解

    目录 一.性能分析类型 1.CPU性能分析 2.内存性能分析 3.阻塞性能分析 二.cpu性能分析 1.生成pporf 2.分析数据 三.内存性能分析 四.benchmark 生成 profile 一.性能分析类型 1.CPU性能分析 CPU性能分析是最常见的性能分析类型.启动CPU分析时,运行时每隔10ms中断一次,采集正在运行协程的堆栈信息. 程序运行结束后,可以根据收集的数据,找到最热代码路径. 一个函数在分析阶段出现的次数越多,则该函数的代码路径(code path)花费的时间占总运行时

  • golang pprof 监控系列 go trace统计原理与使用解析

    目录 引言 go trace 使用 统计原理介绍 Goroutine analysis Execution Network wait Sync block,Blocking syscall,Scheduler wait 各种profile 图 引言 服务监控系列文章 服务监控系列视频 关于go tool trace的使用,网上有相当多的资料,但拿我之前初学golang的经验来讲,很多资料都没有把go tool trace中的相关指标究竟是统计的哪些方法,统计了哪段区间讲解清楚.所以这篇文章不仅仅

  • golang利用pprof与go-torch如何做性能分析

    前言 软件开发过程中,项目上线并不是终点.上线后,还要对程序的取样分析运行情况,并重构现有的功能,让程序执行更高效更稳写. golang的工具包内自带pprof功能,使找出程序中占内存和CPU较多的部分功能方便了不少.加上uber的火焰图,可视化显示,让我们在分析程序时更简单明了. pprof有两个包用来分析程序一个是net/http/pprof另一个是runtime/pprof,net/http/pprof只是对runtime/pprof包进行封装并用http暴露出来,如下图源码所示: 使用n

  • golang pprof 监控goroutine thread统计原理详解

    目录 引言 http 接口暴露的方式 goroutine profile 输出信息介绍 threadcreate 输出信息介绍 程序代码暴露指标信息 统计原理介绍 goroutine fetch 函数实现 threadcreate fetch 函数实现 总结 引言 在之前 golang pprof监控 系列文章里我分别介绍了go trace以及go pprof工具对memory,block,mutex这些维度的统计原理,今天我们接着来介绍golang pprof工具对于goroutine 和th

  • Golang pprof监控之cpu占用率统计原理详解

    目录 http 接口暴露的方式 程序代码生成profile cpu 统计原理分析 线程处理信号的时机 内核发送信号的方式 采样数据的公平性 总结 经过前面的几节对pprof的介绍,对pprof统计的原理算是掌握了七八十了,我们对memory,block,mutex,trace,goroutine,threadcreate这些维度的统计原理都进行了分析,但唯独还没有分析pprof 工具是如何统计cpu使用情况的,今天我们来分析下这部分. http 接口暴露的方式 还记得 golang pprof监

  • web项目中golang性能监控解析

    目录 性能监控 一.web项目(如gin中) 二.单个的go文件如果查看gc 性能监控 一.web项目(如gin中) 1.使用ginpprof import "github.com/DeanThompson/ginpprof" router := gin.Default() ginpprof.Wrap(router) 2.使用pprof 只需要在main.go中引入:_ “net/http/pprof” 访问:127.0.0.1:8080/debug/pprof /debug/ppro

  • 详解golang RWMutex读写互斥锁源码分析

    针对Golang 1.9的sync.RWMutex进行分析,与Golang 1.10基本一样除了将panic改为了throw之外其他的都一样. RWMutex是读写互斥锁.锁可以由任意数量的读取器或单个写入器来保持. RWMutex的零值是一个解锁的互斥锁. 以下代码均去除race竞态检测代码 源代码位置:sync\rwmutex.go 结构体 type RWMutex struct { w Mutex // 互斥锁 writerSem uint32 // 写锁信号量 readerSem uin

  • MySQL 一则慢日志监控误报的问题分析与解决

    之前因为各种原因,有些报警没有引起重视,最近放假马上排除了一些潜在的人为原因,发现数据库的慢日志报警有些奇怪,主要表现是慢日志报警不属实,收到报警的即时通信提醒后,隔一会去数据库里面去排查,发现慢日志的性能似乎没有那么差(我设置的一个阈值是60). 排查过几次代码层面的逻辑,没有发现明显的问题,几次下来,问题依旧,这可激发了修正的念头,决定认真看看到底是什么原因. 后端使用的是基于ORM的模式,数据都存储在模型MySQL_slowlog_sql_history对应的表中. 代码层面是类似如下的逻

  • 使用Java visualVM监控远程JVM的流程分析

    VisualVM是JDK自带的一款全能型性能监控和故障分析工具,包括对CPU使用.JVM堆内存消耗.线程.类加载的实时监控,内存dump文件分析,垃圾回收运行情况的可视化分析等,对故障排查和性能调优很有帮助. 1 监控远程tomcat 对于被监控的tomcat,需要修改tomcat的bin目录下的Catalina.sh配置文件,增加如下配置. JAVA_ OPTS="$JAVA_ OPTS -Dcom.sun.management.jmxremote -Dcom.sun.management.j

  • GoLang日志监控系统实现

    目录 日志监控系统 项目简答介绍 系统架构 读取模块具体实现 日志解析模块 日志监控系统 Nginx(日志文件) -> log_process (实时读取解析写入) -> influxdb(存储) ->grafana(前端日志展示器) influxdb 属于GO语言编写的开源的时序型数据,着力于高性能 查询与存储时序型数据,influxdb 广泛的应用于存储系统的监控数据,IOT行业的实时数据. 目前市面上流行 TSDB(时序型处理数据库):influxDB, TimescaleDB,

  • GoLang中的互斥锁Mutex和读写锁RWMutex使用教程

    目录 一.竞态条件与临界区和同步工具 (1)竞态条件 (2)临界区 (3)同步工具 二.互斥量 三.使用互斥锁的注意事项 (1)使用互斥锁的注意事项 (2)使用defer语句解锁 (3)sync.Mutex是值类型 四.读写锁与互斥锁的异同 (1)读/写互斥锁 (2)读写锁规则 (3)解锁读写锁 一.竞态条件与临界区和同步工具 (1)竞态条件 一旦数据被多个线程共享,那么就会产生冲突和争用的情况,这种情况被称为竞态条件.这往往会破坏数据的一致性. 同步的用途有两个,一个是避免多线程在同一时刻操作

随机推荐