golang?pprof監(jiān)控memory?block?mutex統(tǒng)計原理分析
引言
在上一篇文章 golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用里我講解了這3種性能指標(biāo)如何在程序中暴露以及各自監(jiān)控的范圍。也有提到memory,block,mutex 把這3類數(shù)據(jù)放在一起講,是因為他們統(tǒng)計的原理是很類似的。今天來看看它們究竟是如何統(tǒng)計的。
先說下結(jié)論,這3種類型在runtime內(nèi)部都是通過一個叫做bucket的結(jié)構(gòu)體做的統(tǒng)計,bucket結(jié)構(gòu)體內(nèi)部有指針指向下一個bucket 這樣構(gòu)成了bucket的鏈表,每次分配內(nèi)存,或者每次阻塞產(chǎn)生時,會判斷是否會創(chuàng)建一個新的bucket來記錄此次分配信息。
先來看下bucket里面有哪些信息。
bucket結(jié)構(gòu)體介紹
// 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 }
挨個詳細(xì)解釋下這個bucket結(jié)構(gòu)體: 首先是兩個指針,一個next 指針,一個allnext指針,allnext指針的作用就是形成一個鏈表結(jié)構(gòu),剛才提到的每次記錄分配信息時,如果新增了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
不同的指標(biāo)類型擁有不同的鏈表頭部變量,mbuckets 是內(nèi)存指標(biāo)的鏈表頭,bbuckets 是block指標(biāo)的鏈表頭,xbuckets 是mutex指標(biāo)的鏈表頭。
這里還有個buckethash結(jié)構(gòu),無論那種指標(biāo)類型,只要有bucket結(jié)構(gòu)被創(chuàng)建,那么都將會在buckethash里存上一份,而buckethash用于解決hash沖突的方式則是將沖突的bucket通過指針形成鏈表聯(lián)系起來,這個指針就是剛剛提到的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屬于那種指標(biāo)類型。
hash 則是存儲在buckethash結(jié)構(gòu)內(nèi)的hash值,也是在buckethash 數(shù)組中的索引值。
size 記錄此次分配的大小,對于內(nèi)存指標(biāo)而言有這個值,其余指標(biāo)類型這個值為0。
nstk 則是記錄此次分配時,堆棧信息數(shù)組的大小。還記得在上一講golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用里從網(wǎng)頁看到的堆棧信息嗎。
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 就是記錄的堆棧信息數(shù)組的大小,看到這里,你可能會疑惑,這里僅僅是記錄了堆棧大小,堆棧的內(nèi)容呢?關(guān)于分配信息的記錄呢?
要回答這個問題,得搞清楚創(chuàng)建bucket結(jié)構(gòu)體的時候,內(nèi)存是如何分配的。
首先要明白結(jié)構(gòu)體在進(jìn)行內(nèi)存分配的時候是一塊連續(xù)的內(nèi)存,例如剛才介紹bucket結(jié)構(gòu)體的時候講到的幾個屬性都是在一塊連續(xù)的內(nèi)存上,當(dāng)然,指針指向的地址可以不和結(jié)構(gòu)體內(nèi)存連續(xù),但是指針本身是存儲在這一塊連續(xù)內(nèi)存上的。
接著,我們來看看runtime是如何創(chuàng)建一個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 }
上述代碼是創(chuàng)建一個bucket時源碼, 其中persistentalloc 是runtime內(nèi)部一個用于分配內(nèi)存的方法,底層還是用的mmap,這里就不展開了,只需要知道該方法可以分配一段內(nèi)存,size 則是需要分配的內(nèi)存大小。
persistentalloc返回后的unsafe.Pointer可以強轉(zhuǎn)為bucket類型的指針,unsafe.Pointer是go編譯器允許的 代表指向任意類型的指針 類型。所以關(guān)鍵是看 分配一個bucket結(jié)構(gòu)體的時候,這個size的內(nèi)存空間是如何計算出來的。
首先unsafe.Sizeof 得到分配一個bucket代碼結(jié)構(gòu) 本身所需要的內(nèi)存長度,然后加上了nstk 個uintptr 類型的內(nèi)存長度 ,uintptr代表了一個指針類型,還記得剛剛提到nstk的作用嗎?nstk表明了堆棧信息數(shù)組的大小,而數(shù)組中每個元素就是一個uintptr類型,指向了具體的堆棧位置。
接著判斷 需要創(chuàng)建的bucket的類型,如果是memProfile 內(nèi)存類型 則又用unsafe.Sizeof 得到一個memRecord的結(jié)構(gòu)體所占用的空間大小,如果是blockProfile,或者是mutexProfile 則是在size上加上一個blockRecord結(jié)構(gòu)體占用的空間大小。memRecord和blockRecord 里承載了此次內(nèi)存分配或者此次阻塞行為的詳細(xì)信息。
// 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 }
關(guān)于內(nèi)存分配的詳細(xì)信息最后是有memRecordCycle 承載的,里面有此次內(nèi)存分配的內(nèi)存大小和分配的對象個數(shù)。那memRecord 里的active 和future又有什么含義呢,為啥不干脆用memRecordCycle結(jié)構(gòu)體來表示此次內(nèi)存分配的詳細(xì)信息? 這里我先預(yù)留一個坑,放在下面在解釋,現(xiàn)在你只需要知道,在分配一個內(nèi)存bucket結(jié)構(gòu)體的時候,也分配了一段內(nèi)存空間用于記錄關(guān)于內(nèi)存分配的詳細(xì)信息。
然后再看看blockRecord。
// src/runtime/mprof.go:135 type blockRecord struct { count float64 cycles int64 }
blockRecord 就比較言簡意賅,count代表了阻塞的次數(shù),cycles則代表此次阻塞的周期時長,關(guān)于周期的解釋可以看看我前面一篇文章golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用 ,簡而言之,周期時長是cpu記錄時長的一種方式。你可以把它理解成就是一段時間,不過時間單位不在是秒了,而是一個周期。
可以看到,在計算一個bucket占用的空間的時候,除了bucket結(jié)構(gòu)體本身占用的空間,還預(yù)留了堆??臻g以及memRecord或者blockRecord 結(jié)構(gòu)體占用的內(nèi)存空間大小。
你可能會疑惑,這樣子分配一個bucket結(jié)構(gòu)體,那么如何取出bucket中的memRecord 或者blockRecord結(jié)構(gòu)體呢? 答案是 通過計算memRecord在bucket 中的位置,然后強轉(zhuǎn)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結(jié)構(gòu)體的內(nèi)存占用長度 + 棧數(shù)組占用長度
這一公式成立的前提便是 分配結(jié)構(gòu)體的時候,是連續(xù)的分配了一塊內(nèi)存,所以我們當(dāng)然能通過bucket首部地址以及中間的空間長度計算出memRecord開始的地址。
至此,bucket的結(jié)構(gòu)體描述算是介紹完了,但是還沒有深入到記錄指標(biāo)信息的細(xì)節(jié),下面我們深入研究下記錄細(xì)節(jié),正戲開始。
記錄指標(biāo)細(xì)節(jié)介紹
由于內(nèi)存分配的采樣還是和block阻塞信息的采樣有點點不同,所以我還是決定分兩部分來介紹下,先來看看內(nèi)存分配時,是如何記錄此次內(nèi)存分配信息的。
memory
首先在上篇文章golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用 我介紹過 MemProfileRate ,MemProfileRate 用于控制內(nèi)存分配的采樣頻率,代表平均每分配MemProfileRate字節(jié)便會記錄一次內(nèi)存分配記錄。
當(dāng)觸發(fā)記錄條件時,runtime便會調(diào)用 mProf_Malloc 對此次內(nèi)存分配進(jìn)行記錄,
// 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 則是記錄堆棧的數(shù)組,然后通過 stkbucket 去獲取此次分配的bucket,stkbucket 里會判斷是否先前存在一個相同bucket,如果存在則直接返回。而判斷是否存在相同bucket則是看存量的bucket的分配的內(nèi)存大小和堆棧位置是否和當(dāng)前一致。
// 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結(jié)構(gòu)體,可以知道 buckhash 里容納了程序中所有的bucket,通過一段邏輯算出在bucket的索引值,也就是i的值,然后取出buckhash對應(yīng)索引的鏈表,循環(huán)查找是否有相同bucket。相同則直接返回,不再創(chuàng)建新bucket。
讓我們再回到記錄內(nèi)存分配的主邏輯,stkbucket 方法創(chuàng)建或者獲取 一個bucket之后,會通過mp()方法獲取到其內(nèi)部的memRecord結(jié)構(gòu),然后將此次的內(nèi)存分配的字節(jié)累加到memRecord結(jié)構(gòu)中。
不過這里并不是直接由memRecord 承載累加任務(wù),而是memRecord的memRecordCycle 結(jié)構(gòu)體。
c := mProf.cycle mp := b.mp() mpc := &mp.future[(c+2)%uint32(len(mp.future))] mpc.allocs++ mpc.alloc_bytes += size
這里先是從memRecord 結(jié)構(gòu)體的future結(jié)構(gòu)中取出一個memRecordCycle,然后在memRecordCycle上進(jìn)行累加字節(jié)數(shù),累加分配次數(shù)。
這里有必要介紹下mProf.cycle 和memRecord中的active和future的作用了。
我們知道內(nèi)存分配是一個持續(xù)性的過程,內(nèi)存的回收是由gc定時執(zhí)行的,golang設(shè)計者認(rèn)為,如果每次產(chǎn)生內(nèi)存分配的行為就記錄一次內(nèi)存分配信息,那么很有可能這次分配的內(nèi)存雖然程序已經(jīng)沒有在引用了,但是由于還沒有垃圾回收,所以會造成內(nèi)存分配的曲線就會出現(xiàn)嚴(yán)重的傾斜(因為內(nèi)存只有垃圾回收以后才會被記錄為釋放,也就是memRecordCycle中的free_bytes 才會增加,所以內(nèi)存分配曲線會在gc前不斷增大,gc后出現(xiàn)陡降)。
所以,在記錄內(nèi)存分配信息的時候,是將當(dāng)前的內(nèi)存分配信息經(jīng)過一輪gc后才記錄下來,mProf.cycle 則是當(dāng)前gc的周期數(shù),每次gc時會加1,在記錄內(nèi)存分配時,將當(dāng)前周期數(shù)加2與future取模后的索引值記錄到future ,而在釋放內(nèi)存時,則將 當(dāng)前周期數(shù)加1與future取模后的索引值記錄到future,想想這里為啥要加1才能取到 對應(yīng)的memRecordCycle呢? 因為當(dāng)前的周期數(shù)比起內(nèi)存分配的周期數(shù)已經(jīng)加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) }
在記錄內(nèi)存分配時,只會往future數(shù)組里記錄,那讀取內(nèi)存分配信息的 數(shù)據(jù)時,怎么讀取呢?
還記得memRecord 里有一個類型為memRecordCycle 的active屬性嗎,在讀取的時候,runtime會調(diào)用 mProf_FlushLocked()方法,將當(dāng)前周期的future數(shù)據(jù)讀取到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獲取到了當(dāng)前gc周期,然后用當(dāng)前周期從future里取出 當(dāng)前gc周期的內(nèi)存分配信息 賦值給acitve ,對每個內(nèi)存bucket都進(jìn)行這樣的賦值。
賦值完后,后續(xù)讀取當(dāng)前內(nèi)存分配信息時就只讀active里的數(shù)據(jù)了,至此,算是講完了runtime是如何對內(nèi)存指標(biāo)進(jìn)行統(tǒng)計的。
接著,我們來看看如何對block和mutex指標(biāo)進(jìn)行統(tǒng)計的。
block mutex
block和mutex的統(tǒng)計是由同一個方法,saveblockevent 進(jìn)行記錄的,不過方法內(nèi)部針對這兩種類型還是做了一點點不同的處理。
有必要注意再提一下,mutex是在解鎖unlock時才會記錄一次阻塞行為,而block在記錄mutex鎖阻塞信息時,是在開始執(zhí)行l(wèi)ock調(diào)用的時候記錄的 ,除此以外,block在select 阻塞,channel通道阻塞,wait group 產(chǎn)生阻塞時也會記錄一次阻塞行為。
// 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結(jié)構(gòu)體,然后bp()方法獲取了bucket里的blockRecord 結(jié)構(gòu),并對其count次數(shù)和cycles阻塞周期時長進(jìn)行累加。
// src/runtime/mprof.go:135 type blockRecord struct { count float64 cycles int64 }
注意針對blockProfile 類型的次數(shù)累加 還進(jìn)行了特別的處理,還記得上一篇golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用提到的BlockProfileRate參數(shù)嗎,它是用來設(shè)置block采樣的納秒采樣率的,如果阻塞周期時長cycles小于BlockProfileRate的話,則需要fastrand函數(shù)乘以設(shè)置的納秒時間BlockProfileRate 來決定是否采樣了,所以如果是小于BlockProfileRate 并且saveblockevent進(jìn)行了記錄阻塞信息的話,說明我們只是采樣了部分這樣情況的阻塞,所以次數(shù)用BlockProfileRate 除以 此次阻塞周期時長數(shù),得到一個估算的總的 這類阻塞的次數(shù)。
讀取阻塞信息就很簡單了,直接讀取阻塞bucket的count和周期數(shù)即可。
總結(jié)
至此,算是介紹完了這3種指標(biāo)類型的統(tǒng)計原理,簡而言之,就是通過一個攜帶有堆棧信息的bucket對每次內(nèi)存分配或者阻塞行為進(jìn)行采樣記錄,讀取內(nèi)存分配信息 或者阻塞指標(biāo)信息的 時候便是所有的bucket信息讀取出來。
以上就是golang pprof監(jiān)控memory block mutex統(tǒng)計原理分析的詳細(xì)內(nèi)容,更多關(guān)于golang pprof監(jiān)控統(tǒng)計的資料請關(guān)注腳本之家其它相關(guān)文章!
- golang調(diào)試bug及性能監(jiān)控方式實踐總結(jié)
- golang?pprof?監(jiān)控goroutine?thread統(tǒng)計原理詳解
- golang?pprof監(jiān)控memory?block?mutex使用指南
- golang?pprof?監(jiān)控系列?go?trace統(tǒng)計原理與使用解析
- prometheus?client_go為應(yīng)用程序自定義監(jiān)控指標(biāo)
- web項目中g(shù)olang性能監(jiān)控解析
- Go語言metrics應(yīng)用監(jiān)控指標(biāo)基本使用說明
- Skywalking-go自動監(jiān)控增強使用探究
相關(guān)文章
golang中cache組件的使用及groupcache源碼解析
本篇主要解析groupcache源碼中的關(guān)鍵部分, lru的定義以及如何做到同一個key只加載一次。緩存填充以及加載抑制的實現(xiàn)方法,本文重點給大家介紹golang中cache組件的使用及groupcache源碼解析,感興趣的朋友一起看看吧2021-06-06go語言string轉(zhuǎn)結(jié)構(gòu)體的實現(xiàn)
本文主要介紹了go語言string轉(zhuǎn)結(jié)構(gòu)體的實現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03Go語言的結(jié)構(gòu)體還能這么用?看這篇就夠了
這篇文章主要為大家詳細(xì)介紹了Go語言結(jié)構(gòu)體的各個知識點,最后還介紹了空結(jié)構(gòu)體的3種妙用。文中的示例代碼講解詳細(xì),希望對大家有所幫助2023-02-02詳解go-zero如何使用validator進(jìn)行參數(shù)校驗
這篇文章主要介紹了如何使用validator庫做參數(shù)校驗的一些十分實用的使用技巧,包括翻譯校驗錯誤提示信息、自定義提示信息的字段名稱、自定義校驗方法等,感興趣的可以了解下2024-01-01