<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          為什么 Go 模塊在下游服務抖動恢復后,CPU 占用無法恢復

          共 3494字,需瀏覽 7分鐘

           ·

          2021-08-13 19:57

          團圓節(jié)日公司服務到達歷史峰值 10w+ QPS,而之前沒有預料到營銷系統(tǒng)又在峰值期間搞事情,雪上加霜,流量增長到 11w+ QPS,本組服務差點被打掛(汗

          所幸命大雖然 CPU idle 一度跌至 30 以下,最終還是幸存下來,沒有背上過節(jié)大鍋。與我們的服務代碼寫的好不無關系(拍飛

          事后回顧現(xiàn)場,發(fā)現(xiàn)服務恢復之后整體的 CPU idle 和正常情況下比多消耗了幾個百分點,感覺十分驚詫。恰好又禍不單行,工作日午后碰到下游系統(tǒng)抖動,雖然短時間恢復,我們的系統(tǒng)相比恢復前還是多消耗了兩個百分點。如下圖:

          shake

          確實不太符合直覺,cpu 的使用率上會發(fā)現(xiàn) GC 的各個函數(shù)都比平常用的 cpu 多了那么一點點,那我們只能看看 inuse 是不是有什么變化了,一看倒是嚇了一跳:

          flame

          這個 mstart -> systemstack -> newproc -> malg 顯然是 go func 的時候的函數(shù)調用鏈,按道理來說,創(chuàng)建 goroutine 結構體時,如果可用的 g 和 sudog 結構體能夠復用,會優(yōu)先進行復用:

          func gfput(_p_ *p, gp *g) {
          if readgstatus(gp) != _Gdead {
          throw("gfput: bad status (not Gdead)")
          }

          stksize := gp.stack.hi - gp.stack.lo

          if stksize != _FixedStack {
          // non-standard stack size - free it.
          stackfree(gp.stack)
          gp.stack.lo = 0
          gp.stack.hi = 0
          gp.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 呢?再來看看創(chuàng)建 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,就會創(chuàng)建一個新的 g 結構體,該 g 結構體會被 append 到全局的 allgs 數(shù)組中:

          var (
          allgs []*g
          allglock mutex
          )

          這個 allgs 在什么地方會用到呢:

          GC 的時候:

          func gcResetMarkState() {
          lock(&allglock)
          for _, gp := range allgs {
          gp.gcscandone = false // set to true in gcphasework
          gp.gcscanvalid = false // stack has not been scanned
          gp.gcAssistBytes = 0
          }
          }

          檢查死鎖的時候:

          func checkdead() {
          // ....
          grunning := 0
          lock(&allglock)
          for i := 0; i < len(allgs); i++ {
          gp := allgs[i]
          if isSystemGoroutine(gp, false) {
          continue
          }
          }
          }

          檢查死鎖這個操作在每次 sysmon、線程創(chuàng)建、線程進 idle 隊列的時候都會調用,調用頻率也不能說特別低。

          翻閱了所有 allgs 的引用代碼,發(fā)現(xiàn)該數(shù)組創(chuàng)建之后,并不會收縮。

          我們可以根據(jù)上面看到的所有代碼,來還原這種抖動情況下整個系統(tǒng)的情況了:

          1. 下游系統(tǒng)超時,很多 g 都被阻塞了,掛在 gopark 上,相當于提高了系統(tǒng)的并發(fā)

          2. 因為 gFree 沒法復用,導致創(chuàng)建了比平時更多的 goroutine(具體有多少,就看你超時設置了多少

          3. 抖動時創(chuàng)建的 goroutine 會進入全局 allgs 數(shù)組,該數(shù)組不會進行收縮,且每次 gc、sysmon、死鎖檢查期間都會進行全局掃描

          4. 上述全局掃描導致我們的系統(tǒng)在下游系統(tǒng)抖動恢復之后,依然要去掃描這些抖動時創(chuàng)建的 g 對象,使 cpu 占用升高,idle 降低。

          5. 只能重啟(重啟大法好

          看起來并沒有什么解決辦法,如果想要復現(xiàn)這個問題的讀者,可以試一下下面這個程序:

          package main

          import (
          "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 和碼農桃花源:



          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  18禁一区二区 | 麻豆国产精品无码人妻无码 | 97超碰人人模人人人爽人人爱 | 韩国三级黄片视频 | 亚洲无码 国产精品 豆花 |