您好,登錄后才能下訂單哦!
本文小編為大家詳細介紹“Golang pprof監控之cpu占用率統計原理是什么”,內容詳細,步驟清晰,細節處理妥當,希望這篇“Golang pprof監控之cpu占用率統計原理是什么”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學習新知識吧。
還記得 golang pprof監控系列(2) —— memory,block,mutex 使用 里我們啟動了一個http服務來暴露各種性能指標信息。讓我們再回到當時啟動http服務看到的網頁圖。
當點擊上圖中profile鏈接時,便會下載一個關于cpu指標信息的二進制文件。這個二進制文件同樣可以用go tool pprof 工具去分析,同樣,關于go tool pprof的使用不是本文的重點,網上的資料也相當多,所以我略去了這部分。
緊接著,我們來快速看下如何用程序代碼的方式生成cpu的profile文件。
os.Remove("cpu.out") f, _ := os.Create("cpu.out") defer f.Close() pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // .... do other things
代碼比較簡單,pprof.StartCPUProfile 則開始統計 cpu使用情況,pprof.StopCPUProfile則停止統計cpu使用情況,將程序使用cpu的情況寫入cpu.out文件。cpu.out文件我們則可以用go tool pprof去分析了。
好的,在快速的看完如何在程序中暴露cpu性能指標后,我們來看看golang是如何統計各個函數cpu使用情況的。接下來,正戲開始。
首先要明白,我們究竟要統計的是什么內容?我們需要知道cpu的使用情況,換言之就是cpu的工作時間花在了哪些函數上,最后是不是就是看函數在cpu上的工作時長。
那么函數的在cpu上工作時長應該如何去進行統計?
golang還是采用部分采樣的方式,通過settimmer 系統調用設置了 發送SIGPROF 的定時器,當達到runtime.SetCPUProfileRate設置的周期間隔時,操作系統就會向進程發送SIGPROF 信號,默認情況下是100Mz(10毫秒)。
一旦設置了 發送SIGPROF信號的定時器,操作系統便會定期向進程發送SIGPROF信號。
設置定時器的代碼便是在我們調用pprof.StartCPUProfile方法開啟cpu信息采樣的時候。代碼如下,
// src/runtime/pprof/pprof.go:760 func StartCPUProfile(w io.Writer) error { const hz = 100 cpu.Lock() defer cpu.Unlock() if cpu.done == nil { cpu.done = make(chan bool) } // Double-check. if cpu.profiling { return fmt.Errorf("cpu profiling already in use") } cpu.profiling = true runtime.SetCPUProfileRate(hz) go profileWriter(w) return nil }
在倒數第三行的時候調用了設置采樣的周期,并且緊接著profileWriter 就是用一個協程啟動后去不斷的讀取cpu的采樣數據寫到文件里。而調用settimer的地方就是在runtime.SetCPUProfileRate里,runtime.SetCPUProfileRate最終會調用 setcpuprofilerate方法 ,setcpuprofilerate 又會去調用setProcessCPUProfiler方法設置settimer 定時器。
// src/runtime/signal_unix.go:269 func setProcessCPUProfiler(hz int32) { ..... var it itimerval it.it_interval.tv_sec = 0 it.it_interval.set_usec(1000000 / hz) it.it_value = it.it_interval setitimer(_ITIMER_PROF, &it, nil) ....
經過上述步驟后,cpu的采樣就真正開始了,之后就是定時器被觸發送SIGPROF信號,進程接收到這個信號后,會對當前函數的調用堆棧進行記錄,由于默認的采樣周期設置的是100Mz,所以,你可以理解為每10ms,golang就會統計下當前正在運行的是哪個函數,在采樣的這段時間內,哪個函數被統計的次數越多,是不是就能說明這個函數在這段時間內占用cpu的工作時長就越多了。
由于golang借助了linux的信號機制去進行cpu執行函數的采樣,這里有必要額外介紹下linux 進程與信號相關的知識。首先來看下線程處理信號的時機在什么時候。
線程對信號的處理時機一般 是在由內核態返回到用戶態之前,也就是說,當線程由于系統調用或者中斷進入內核態后, 當系統調用結束或者中斷處理完成后,在返回到用戶態之前,操作系統會檢查這個線程是不是有未處理的信號,如果有的話,那么會先切回到用戶態讓 線程會首先處理信號,信號處理完畢后 又返回內核態,內核此時才會將調用棧設置為中斷或者系統調用時 用戶進程中斷的地方 ,然后切換到用戶態后就繼續在用戶進程之前中斷的地方繼續執行程序邏輯了。由于進程幾乎每時每刻都在進行諸如系統調用的工作,可以認為,信號的處理是幾乎實時的。 如下是線程內核態與用戶態切換的過程,正式信號處理檢查的地方。整個過程可以用下面的示意圖表示。
知道了信號是如何被線程處理的,還需要了解下,內核會如何發送信號給進程。
內核向進程發信號的方式是對進程中的一個線程發送信號,而通過settimmer 系統調用設置定時器 發送SIGPROF 信號的方式就是隨機的對進程中的一個運行中線程去進行發送。而運行中線程接收到這個信號后,就調用自身的處理函數對這個信號去進行處理,對于SIGPROF 信號而言,則是將線程中斷前的函數棧記錄下來,用于后續分析函數占用cpu的工作時長。
由于只是隨機的向一個運行中的線程發送SIGPROF 信號,這里涉及到了兩個問題?
第一因為同一個進程中只有一個線程在進行采樣,所以在隨機選擇運行線程發送SIGPROF信號時,要求選擇線程時的公平性,不然可能會出現A,B兩個線程,A線程接收到SIGPROF信號的次數遠遠大于B 線程接收SIGPROF信號的次數,這樣對A線程進行采樣的次數將會變多,影響了我們采樣的結果。
而golang用settimmer 設置定時器發送SIGPROF 信號 的方式的確被證實在linux上存在線程選擇公平性問題(但是mac os上沒有這個問題) 關于這個問題的討論在github上有記錄,這是鏈接 這個問題已經在go1.18上得到了解決,解決方式我會在下面給出,我們先來看隨機的向一個運行中的線程發送SIGPROF 信號 引發的第二個問題。
第二 因為是向一個運行中的線程去發送信號,所以我們只能統計到采樣時間段內在cpu上運行的函數,而那些io阻塞的函數將不能被統計到,關于這點業內已經有開源庫幫助解決,https://github.com/felixge/fgprof,不過由于這個庫進行采樣時會stop the world ,所以其作者強烈建議如果go協程數量比較多時,將go版本升級到1.19再使用。后續有機會再來探討這個庫的實現吧,我們先回到如何解決settimer函數在選擇線程的公平性問題上。
為了解決公平性問題,golang在settimer的系統調用的基礎上增加了timer_create系統調用timer_create 可以單獨的為每一個線程都創建定時器,這樣每個運行線程都會采樣到自己的函數堆棧了。所以在go1.18版本對pprof.StartCPUProfile內部創建定時器的代碼進行了改造。剛才有提到pprof.StartCPUProfile 底層其實是調用setcpuprofilerate 這個方法去設置的定時器,所以我們來看看go1.18和go1.17版本在這個方法的實現上主要是哪里不同。
// go1.17 版本 src/runtime/proc.go:4563 func setcpuprofilerate(hz int32) { if hz < 0 { hz = 0 } _g_ := getg() _g_.m.locks++ setThreadCPUProfiler(0) for !atomic.Cas(&prof.signalLock, 0, 1) { osyield() } if prof.hz != hz { // 設置進程維度的 SIGPROF 信號發送器 setProcessCPUProfiler(hz) prof.hz = hz } atomic.Store(&prof.signalLock, 0) lock(&sched.lock) sched.profilehz = hz unlock(&sched.lock) if hz != 0 { // 設置線程維度的SIGPROF 信號定時器 setThreadCPUProfiler(hz) } _g_.m.locks-- }
上述是go1.17版本的setcpuprofilerate 代碼,如果你再去看 go1.18版本的代碼,會發現他們在這個方法上是一模一樣的,都是調用了setProcessCPUProfiler 和setThreadCPUProfiler,setProcessCPUProfiler 設置進程維度的發送SIGPROF信號定時器,setThreadCPUProfiler設置線程維度的發送SIGPROF信號的定時器,但其實setThreadCPUProfiler 在go1.17的實現上并不完整。
// go 1.17 src/runtime/signal_unix.go:314 func setThreadCPUProfiler(hz int32) { getg().m.profilehz = hz }
go1.17版本上僅僅是為協程里代表線程的m變量設置了一個profilehz(采樣的頻率),并沒有真正實現線程維度的采樣。
// go 1.18 src/runtime/os_linux.go:605 .... // setThreadCPUProfiler 方法內部 timer_create的代碼段 var timerid int32 var sevp sigevent sevp.notify = _SIGEV_THREAD_ID sevp.signo = _SIGPROF sevp.sigev_notify_thread_id = int32(mp.procid) ret := timer_create(_CLOCK_THREAD_CPUTIME_ID, &sevp, &timerid) if ret != 0 { return } ....
在go1.18版本上的setThreadCPUProfiler則真正實現了這部分邏輯,由于go1.18版本它同時調用了setProcessCPUProfiler以及setThreadCPUProfiler,這樣在接收SIGPROF信號時就會出現重復計數的問題。
所以go1.18在處理SIGPROF信號的時候也做了去重處理,所以在golang信號處理的方法sighandler 內部有這樣一段邏輯。
func sighandler(sig uint32, info *siginfo, ctxt unsafe.Pointer, gp *g) { _g_ := getg() c := &sigctxt{info, ctxt} if sig == _SIGPROF { mp := _g_.m // Some platforms (Linux) have per-thread timers, which we use in // combination with the process-wide timer. Avoid double-counting. if validSIGPROF(mp, c) { sigprof(c.sigpc(), c.sigsp(), c.siglr(), gp, mp) } return } .....
如果發現信號是_SIGPROF 那么會通過validSIGPROF 去檢測此次的_SIGPROF信號是否應該被統計。validSIGPROF的檢測邏輯這里就不展開了。
讀到這里,這篇“Golang pprof監控之cpu占用率統計原理是什么”文章已經介紹完畢,想要掌握這篇文章的知識點還需要大家自己動手實踐使用過才能領會,如果想了解更多相關內容的文章,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。