Go?模塊在下游服務(wù)抖動恢復(fù)后CPU占用無法恢復(fù)原因
引言
某團圓節(jié)日公司服務(wù)到達歷史峰值 10w+ QPS,而之前沒有預(yù)料到營銷系統(tǒng)又在峰值期間搞事情,雪上加霜,流量增長到 11w+ QPS,本組服務(wù)差點被打掛(汗
所幸命大雖然 CPU idle 一度跌至 30 以下,最終還是幸存下來,沒有背上過節(jié)大鍋。與我們的服務(wù)代碼寫的好不無關(guān)系(拍飛
事后回顧現(xiàn)場,發(fā)現(xiàn)服務(wù)恢復(fù)之后整體的 CPU idle 和正常情況下比多消耗了幾個百分點,感覺十分驚詫。恰好又禍不單行,工作日午后碰到下游系統(tǒng)抖動,雖然短時間恢復(fù),我們的系統(tǒng)相比恢復(fù)前還是多消耗了兩個百分點。如下圖:

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

這個 mstart -> systemstack -> newproc -> malg 顯然是 go func 的時候的函數(shù)調(diào)用鏈,按道理來說,創(chuàng)建 goroutine 結(jié)構(gòu)體時,如果可用的 g 和 sudog 結(jié)構(gòu)體能夠復(fù)用,會優(yōu)先進行復(fù)用:
優(yōu)先復(fù)用
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
}
創(chuàng)建 g
怎么會出來這么多 malg 呢?再來看看創(chuàng)建 g 的代碼:
func newproc1(fn *funcval, argp *uint8, narg int32, callergp *g, callerpc uintptr) {
_g_ := getg()
// .... 省略無關(guān)代碼
_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 結(jié)構(gòu)體,該 g 結(jié)構(gòu)體會被 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)建 templateThread、線程進 idle 隊列的時候都會調(diào)用,調(diào)用頻率也不能說特別低。
翻閱了所有 allgs 的引用代碼,發(fā)現(xiàn)該數(shù)組創(chuàng)建之后,并不會收縮。
我們可以根據(jù)上面看到的所有代碼,來還原這種抖動情況下整個系統(tǒng)的情況了:
- 下游系統(tǒng)超時,很多 g 都被阻塞了,掛在 gopark 上,相當于提高了系統(tǒng)的并發(fā)
- 因為 gFree 沒法復(fù)用,導(dǎo)致創(chuàng)建了比平時更多的 goroutine(具體有多少,就看你超時設(shè)置了多少
- 抖動時創(chuàng)建的 goroutine 會進入全局 allgs 數(shù)組,該數(shù)組不會進行收縮,且每次 gc、sysmon、死鎖檢查期間都會進行全局掃描
- 上述全局掃描導(dǎo)致我們的系統(tǒng)在下游系統(tǒng)抖動恢復(fù)之后,依然要去掃描這些抖動時創(chuàng)建的 g 對象,使 cpu 占用升高,idle 降低。
- 只能重啟
看起來并沒有什么解決辦法,如果想要復(fù)現(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 依然有百萬之巨。

循環(huán)查看單個進程的 cpu 消耗:
import psutil
import time
p = psutil.Process(1) # 改成你自己的 pid 就行了
while 1:
v = str(p.cpu_percent())
if "0.0" != v:
print(v, time.time())
time.sleep(1)
以上就是Go 模塊在下游服務(wù)抖動恢復(fù)后CPU占用無法恢復(fù)原因的詳細內(nèi)容,更多關(guān)于Go CPU占用無法恢復(fù)原因的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Go語言RPC Authorization進行簡單ip安全驗證的方法
這篇文章主要介紹了Go語言RPC Authorization進行簡單ip安全驗證的方法,實例分析了Go語言進行ip驗證的技巧,需要的朋友可以參考下2015-03-03
使用VSCODE配置GO語言開發(fā)環(huán)境的完整步驟
Go語言是采用UTF8編碼的,理論上使用任何文本編輯器都能做Go語言開發(fā),大家可以根據(jù)自己的喜好自行選擇,下面這篇文章主要給大家介紹了關(guān)于使用VSCODE配置GO語言開發(fā)環(huán)境的完整步驟,需要的朋友可以參考下2022-11-11
Go語言操作數(shù)據(jù)庫及其常規(guī)操作的示例代碼
這篇文章主要介紹了Go語言操作數(shù)據(jù)庫及其常規(guī)操作的示例代碼,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-04-04

