您好,登錄后才能下訂單哦!
這篇“golang pprof監控memory block mutex統計原理是什么”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“golang pprof監控memory block mutex統計原理是什么”文章吧。
// 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阻塞信息的采樣有點點不同,所以我還是決定分兩部分來介紹下,先來看看內存分配時,是如何記錄此次內存分配信息的。
首先在上篇文章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的統計是由同一個方法,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和周期數即可。
以上就是關于“golang pprof監控memory block mutex統計原理是什么”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。