为什么 Go 模块在下游服务抖动恢复后,CPU 占用无法恢复
某團圓節日公司服務到達歷史峰值 10w+ QPS,而之前沒有預料到營銷系統又在峰值期間搞事情,雪上加霜,流量增長到 11w+ QPS,本組服務差點被打掛(汗
所幸命大雖然 CPU idle 一度跌至 30 以下,最終還是幸存下來,沒有背上過節大鍋。與我們的服務代碼寫的好不無關系(拍飛
事后回顧現場,發現服務恢復之后整體的 CPU idle 和正常情況下比多消耗了幾個百分點,感覺十分驚詫。恰好又禍不單行,工作日午后碰到下游系統抖動,雖然短時間恢復,我們的系統相比恢復前還是多消耗了兩個百分點。如下圖:
shake
確實不太符合直覺,cpu 的使用率上會發現 GC 的各個函數都比平常用的 cpu 多了那么一點點,那我們只能看看 inuse 是不是有什么變化了,一看倒是嚇了一跳:
flame
這個 mstart -> systemstack -> newproc -> malg 顯然是 go func 的時候的函數調用鏈,按道理來說,創建 goroutine 結構體時,如果可用的 g 和 sudog 結構體能夠復用,會優先進行復用:
func gfput(_p_ *p, gp *g) {if readgstatus(gp) != _Gdead {throw("gfput: bad status (not Gdead)")}stksize := gp.stack.hi - gp.stack.loif stksize != _FixedStack {// non-standard stack size - free it.stackfree(gp.stack)gp.stack.lo = 0gp.stack.hi = 0gp.stackguard0 = 0}_p_.gFree.push(gp)_p_.gFree.n++if _p_.gFree.n >= 64 {lock(&sched.gFree.lock)for _p_.gFree.n >= 32 {_p_.gFree.n--gp = _p_.gFree.pop()if gp.stack.lo == 0 {sched.gFree.noStack.push(gp)} else {sched.gFree.stack.push(gp)}sched.gFree.n++}unlock(&sched.gFree.lock)} }func gfget(_p_ *p) *g { retry:if _p_.gFree.empty() && (!sched.gFree.stack.empty() || !sched.gFree.noStack.empty()) {lock(&sched.gFree.lock)for _p_.gFree.n < 32 {// Prefer Gs with stacks.gp := sched.gFree.stack.pop()if gp == nil {gp = sched.gFree.noStack.pop()if gp == nil {break}}sched.gFree.n--_p_.gFree.push(gp)_p_.gFree.n++}unlock(&sched.gFree.lock)goto retry}gp := _p_.gFree.pop()if gp == nil {return nil}_p_.gFree.n--if gp.stack.lo == 0 {systemstack(func() {gp.stack = stackalloc(_FixedStack)})gp.stackguard0 = gp.stack.lo + _StackGuard} else {// ....}return gp }怎么會出來這么多 malg 呢?再來看看創建 g 的代碼:
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {_g_ := getg()// .... 省略無關代碼_p_ := _g_.m.p.ptr()newg := gfget(_p_)if newg == nil {newg = malg(_StackMin)casgstatus(newg, _Gidle, _Gdead)allgadd(newg) // 重點在這里} }一旦在 當前 p 的 gFree 和全局的 gFree 找不到可用的 g,就會創建一個新的 g 結構體,該 g 結構體會被 append 到全局的 allgs 數組中:
var (allgs []*gallglock mutex )這個 allgs 在什么地方會用到呢:
GC 的時候:
func gcResetMarkState() {lock(&allglock)for _, gp := range allgs {gp.gcscandone = false // set to true in gcphaseworkgp.gcscanvalid = false // stack has not been scannedgp.gcAssistBytes = 0} }檢查死鎖的時候:
func checkdead() {// ....grunning := 0lock(&allglock)for i := 0; i < len(allgs); i++ {gp := allgs[i]if isSystemGoroutine(gp, false) {continue}} }檢查死鎖這個操作在每次 sysmon、線程創建、線程進 idle 隊列的時候都會調用,調用頻率也不能說特別低。
翻閱了所有 allgs 的引用代碼,發現該數組創建之后,并不會收縮。
我們可以根據上面看到的所有代碼,來還原這種抖動情況下整個系統的情況了:
下游系統超時,很多 g 都被阻塞了,掛在 gopark 上,相當于提高了系統的并發
因為 gFree 沒法復用,導致創建了比平時更多的 goroutine(具體有多少,就看你超時設置了多少
抖動時創建的 goroutine 會進入全局 allgs 數組,該數組不會進行收縮,且每次 gc、sysmon、死鎖檢查期間都會進行全局掃描
上述全局掃描導致我們的系統在下游系統抖動恢復之后,依然要去掃描這些抖動時創建的 g 對象,使 cpu 占用升高,idle 降低。
只能重啟(重啟大法好
看起來并沒有什么解決辦法,如果想要復現這個問題的讀者,可以試一下下面這個程序:
package mainimport ("log""net/http"_ "net/http/pprof""time" )func sayhello(wr http.ResponseWriter, r *http.Request) {}func main() {for i := 0; i < 1000000; i++ {go func() {time.Sleep(time.Second * 10)}()}http.HandleFunc("/", sayhello)err := http.ListenAndServe(":9090", nil)if err != nil {log.Fatal("ListenAndServe:", err)} }啟動后等待 10s,待所有 goroutine 都散過后,pprof 的 inuse 的 malg 依然有百萬之巨。
歡迎關注 TechPaper 和碼農桃花源:
總結
以上是生活随笔為你收集整理的为什么 Go 模块在下游服务抖动恢复后,CPU 占用无法恢复的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 从容器到容器云,什么才是 Kuberne
- 下一篇: 曹大带我学 Go(11)—— 从 map