網(wǎng)站首頁 編程語言 正文
引言
某團(tuán)圓節(jié)日公司服務(wù)到達(dá)歷史峰值 10w+ QPS,而之前沒有預(yù)料到營(yíng)銷系統(tǒng)又在峰值期間搞事情,雪上加霜,流量增長(zhǎng)到 11w+ QPS,本組服務(wù)差點(diǎn)被打掛(汗
所幸命大雖然 CPU idle 一度跌至 30 以下,最終還是幸存下來,沒有背上過節(jié)大鍋。與我們的服務(wù)代碼寫的好不無關(guān)系(拍飛
事后回顧現(xiàn)場(chǎng),發(fā)現(xiàn)服務(wù)恢復(fù)之后整體的 CPU idle 和正常情況下比多消耗了幾個(gè)百分點(diǎn),感覺十分驚詫。恰好又禍不單行,工作日午后碰到下游系統(tǒng)抖動(dòng),雖然短時(shí)間恢復(fù),我們的系統(tǒng)相比恢復(fù)前還是多消耗了兩個(gè)百分點(diǎn)。如下圖:
確實(shí)不太符合直覺,cpu 的使用率上會(huì)發(fā)現(xiàn) GC 的各個(gè)函數(shù)都比平常用的 cpu 多了那么一點(diǎn)點(diǎn),那我們只能看看 inuse 是不是有什么變化了,一看倒是嚇了一跳:
這個(gè)?mstart -> systemstack -> newproc -> malg
?顯然是 go func 的時(shí)候的函數(shù)調(diào)用鏈,按道理來說,創(chuàng)建 goroutine 結(jié)構(gòu)體時(shí),如果可用的 g 和 sudog 結(jié)構(gòu)體能夠復(fù)用,會(huì)優(yōu)先進(jìn)行復(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
怎么會(huì)出來這么多 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) // 重點(diǎn)在這里 } }
一旦在 當(dāng)前 p 的 gFree 和全局的 gFree 找不到可用的 g,就會(huì)創(chuàng)建一個(gè)新的 g 結(jié)構(gòu)體,該 g 結(jié)構(gòu)體會(huì)被 append 到全局的 allgs 數(shù)組中:
var ( allgs []*g allglock mutex )
allgs 在什么地方會(huì)用到
GC 的時(shí)候
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 } }
檢查死鎖的時(shí)候:
func checkdead() { // .... grunning := 0 lock(&allglock) for i := 0; i < len(allgs); i++ { gp := allgs[i] if isSystemGoroutine(gp, false) { continue } } }
檢查死鎖這個(gè)操作在每次 sysmon、創(chuàng)建 templateThread、線程進(jìn) idle 隊(duì)列的時(shí)候都會(huì)調(diào)用,調(diào)用頻率也不能說特別低。
翻閱了所有 allgs 的引用代碼,發(fā)現(xiàn)該數(shù)組創(chuàng)建之后,并不會(huì)收縮。
我們可以根據(jù)上面看到的所有代碼,來還原這種抖動(dòng)情況下整個(gè)系統(tǒng)的情況了:
- 下游系統(tǒng)超時(shí),很多 g 都被阻塞了,掛在 gopark 上,相當(dāng)于提高了系統(tǒng)的并發(fā)
- 因?yàn)?gFree 沒法復(fù)用,導(dǎo)致創(chuàng)建了比平時(shí)更多的 goroutine(具體有多少,就看你超時(shí)設(shè)置了多少
- 抖動(dòng)時(shí)創(chuàng)建的 goroutine 會(huì)進(jìn)入全局 allgs 數(shù)組,該數(shù)組不會(huì)進(jìn)行收縮,且每次 gc、sysmon、死鎖檢查期間都會(huì)進(jìn)行全局掃描
- 上述全局掃描導(dǎo)致我們的系統(tǒng)在下游系統(tǒng)抖動(dòng)恢復(fù)之后,依然要去掃描這些抖動(dòng)時(shí)創(chuàng)建的 g 對(duì)象,使 cpu 占用升高,idle 降低。
- 只能重啟
看起來并沒有什么解決辦法,如果想要復(fù)現(xiàn)這個(gè)問題的讀者,可以試一下下面這個(gè)程序:
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) } }
啟動(dòng)后等待 10s,待所有 goroutine 都散過后,pprof 的 inuse 的 malg 依然有百萬之巨。
循環(huán)查看單個(gè)進(jì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)
原文鏈接:https://xargin.com/cpu-idle-cannot-recover-after-peak-load/
相關(guān)推薦
- 2022-06-17 C#關(guān)鍵字in、out、ref的作用與區(qū)別_C#教程
- 2022-03-13 用Docker搭建nextcloud個(gè)人網(wǎng)盤教程_docker
- 2022-05-13 分布式架構(gòu)Redis中有哪些數(shù)據(jù)結(jié)構(gòu)及底層實(shí)現(xiàn)原理_Redis
- 2024-01-13 nvm命令
- 2022-03-29 帶你了解C++中vector的用法_C 語言
- 2022-10-15 python?FastApi實(shí)現(xiàn)數(shù)據(jù)表遷移流程詳解_python
- 2022-03-20 C語言基礎(chǔ)雙指針移除元素解法_C 語言
- 2023-10-17 My-form組件,基于element傳參展示用于后臺(tái)管理篩選的表單框
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支