golang?pprof監(jiān)控memory?block?mutex統(tǒng)計原理分析
引言
在上一篇文章 golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用里我講解了這3種性能指標如何在程序中暴露以及各自監(jiān)控的范圍。也有提到memory,block,mutex 把這3類數據放在一起講,是因為他們統(tǒng)計的原理是很類似的。今天來看看它們究竟是如何統(tǒng)計的。
先說下結論,這3種類型在runtime內部都是通過一個叫做bucket的結構體做的統(tǒng)計,bucket結構體內部有指針指向下一個bucket 這樣構成了bucket的鏈表,每次分配內存,或者每次阻塞產生時,會判斷是否會創(chuàng)建一個新的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結構被創(chuàng)建,那么都將會在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監(jiān)控系列(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 就是記錄的堆棧信息數組的大小,看到這里,你可能會疑惑,這里僅僅是記錄了堆棧大小,堆棧的內容呢?關于分配信息的記錄呢?
要回答這個問題,得搞清楚創(chuàng)建bucket結構體的時候,內存是如何分配的。
首先要明白結構體在進行內存分配的時候是一塊連續(xù)的內存,例如剛才介紹bucket結構體的時候講到的幾個屬性都是在一塊連續(xù)的內存上,當然,指針指向的地址可以不和結構體內存連續(xù),但是指針本身是存儲在這一塊連續(xù)內存上的。
接著,我們來看看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內部一個用于分配內存的方法,底層還是用的mmap,這里就不展開了,只需要知道該方法可以分配一段內存,size 則是需要分配的內存大小。
persistentalloc返回后的unsafe.Pointer可以強轉為bucket類型的指針,unsafe.Pointer是go編譯器允許的 代表指向任意類型的指針 類型。所以關鍵是看 分配一個bucket結構體的時候,這個size的內存空間是如何計算出來的。
首先unsafe.Sizeof 得到分配一個bucket代碼結構 本身所需要的內存長度,然后加上了nstk 個uintptr 類型的內存長度 ,uintptr代表了一個指針類型,還記得剛剛提到nstk的作用嗎?nstk表明了堆棧信息數組的大小,而數組中每個元素就是一個uintptr類型,指向了具體的堆棧位置。
接著判斷 需要創(chuàng)建的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監(jiān)控系列(2) —— memory,block,mutex 使用 ,簡而言之,周期時長是cpu記錄時長的一種方式。你可以把它理解成就是一段時間,不過時間單位不在是秒了,而是一個周期。
可以看到,在計算一個bucket占用的空間的時候,除了bucket結構體本身占用的空間,還預留了堆??臻g以及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結構體的內存占用長度 + 棧數組占用長度
這一公式成立的前提便是 分配結構體的時候,是連續(xù)的分配了一塊內存,所以我們當然能通過bucket首部地址以及中間的空間長度計算出memRecord開始的地址。
至此,bucket的結構體描述算是介紹完了,但是還沒有深入到記錄指標信息的細節(jié),下面我們深入研究下記錄細節(jié),正戲開始。
記錄指標細節(jié)介紹
由于內存分配的采樣還是和block阻塞信息的采樣有點點不同,所以我還是決定分兩部分來介紹下,先來看看內存分配時,是如何記錄此次內存分配信息的。
memory
首先在上篇文章golang pprof監(jiān)控系列(2) —— memory,block,mutex 使用 我介紹過 MemProfileRate ,MemProfileRate 用于控制內存分配的采樣頻率,代表平均每分配MemProfileRate字節(jié)便會記錄一次內存分配記錄。
當觸發(fā)記錄條件時,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對應索引的鏈表,循環(huán)查找是否有相同bucket。相同則直接返回,不再創(chuàng)建新bucket。
讓我們再回到記錄內存分配的主邏輯,stkbucket 方法創(chuàng)建或者獲取 一個bucket之后,會通過mp()方法獲取到其內部的memRecord結構,然后將此次的內存分配的字節(jié)累加到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上進行累加字節(jié)數,累加分配次數。
這里有必要介紹下mProf.cycle 和memRecord中的active和future的作用了。
我們知道內存分配是一個持續(xù)性的過程,內存的回收是由gc定時執(zhí)行的,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都進行這樣的賦值。
賦值完后,后續(xù)讀取當前內存分配信息時就只讀active里的數據了,至此,算是講完了runtime是如何對內存指標進行統(tǒng)計的。
接著,我們來看看如何對block和mutex指標進行統(tǒng)計的。
block mutex
block和mutex的統(tǒng)計是由同一個方法,saveblockevent 進行記錄的,不過方法內部針對這兩種類型還是做了一點點不同的處理。
有必要注意再提一下,mutex是在解鎖unlock時才會記錄一次阻塞行為,而block在記錄mutex鎖阻塞信息時,是在開始執(zhí)行l(wèi)ock調用的時候記錄的 ,除此以外,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監(jiān)控系列(2) —— memory,block,mutex 使用提到的BlockProfileRate參數嗎,它是用來設置block采樣的納秒采樣率的,如果阻塞周期時長cycles小于BlockProfileRate的話,則需要fastrand函數乘以設置的納秒時間BlockProfileRate 來決定是否采樣了,所以如果是小于BlockProfileRate 并且saveblockevent進行了記錄阻塞信息的話,說明我們只是采樣了部分這樣情況的阻塞,所以次數用BlockProfileRate 除以 此次阻塞周期時長數,得到一個估算的總的 這類阻塞的次數。
讀取阻塞信息就很簡單了,直接讀取阻塞bucket的count和周期數即可。
總結
至此,算是介紹完了這3種指標類型的統(tǒng)計原理,簡而言之,就是通過一個攜帶有堆棧信息的bucket對每次內存分配或者阻塞行為進行采樣記錄,讀取內存分配信息 或者阻塞指標信息的 時候便是所有的bucket信息讀取出來。
以上就是golang pprof監(jiān)控memory block mutex統(tǒng)計原理分析的詳細內容,更多關于golang pprof監(jiān)控統(tǒng)計的資料請關注腳本之家其它相關文章!
相關文章
golang中cache組件的使用及groupcache源碼解析
本篇主要解析groupcache源碼中的關鍵部分, lru的定義以及如何做到同一個key只加載一次。緩存填充以及加載抑制的實現方法,本文重點給大家介紹golang中cache組件的使用及groupcache源碼解析,感興趣的朋友一起看看吧2021-06-06

