目录
引言优先复用创建 gallgs 在什么地方会用到引言
某团圆节日公司服务到达历史峰值 10w+ QPS,而之前没有预料到营销系统又在峰值期间搞事情,雪上加霜,流量增长到 11w+ QPS,本组服务差点被打挂(汗
所幸命大虽然 CPU idle 一度跌至 30 以下,最终还是幸存下来,没有背上过节大锅。与我们的服务代码写的好不无关系(拍飞
事后回顾现场,发现服务恢复之后整体的 CPU idle 和正常情况下比多消耗了几个百分点,感觉十分惊诧。恰好又祸不单行,工作日午后碰到下游系统抖动,虽然短时间恢复,我们的系统相比恢复前还是多消耗了两个百分点。如下图:
(相关资料图)
确实不太符合直觉,cpu 的使用率上会发现 GC 的各个函数都比平常用的 cpu 多了那么一点点,那我们只能看看 inuse 是不是有什么变化了,一看倒是吓了一跳:
这个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.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 }
创建 g
怎么会出来这么多 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 []*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、创建 templateThread、线程进 idle 队列的时候都会调用,调用频率也不能说特别低。
翻阅了所有 allgs 的引用代码,发现该数组创建之后,并不会收缩。
我们可以根据上面看到的所有代码,来还原这种抖动情况下整个系统的情况了:
下游系统超时,很多 g 都被阻塞了,挂在 gopark 上,相当于提高了系统的并发因为 gFree 没法复用,导致创建了比平时更多的 goroutine(具体有多少,就看你超时设置了多少抖动时创建的 goroutine 会进入全局 allgs 数组,该数组不会进行收缩,且每次 gc、sysmon、死锁检查期间都会进行全局扫描上述全局扫描导致我们的系统在下游系统抖动恢复之后,依然要去扫描这些抖动时创建的 g 对象,使 cpu 占用升高,idle 降低。只能重启看起来并没有什么解决办法,如果想要复现这个问题的读者,可以试一下下面这个程序:
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 依然有百万之巨。
循环查看单个进程的 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 模块在下游服务抖动恢复后CPU占用无法恢复原因的详细内容,更多关于Go CPU占用无法恢复原因的资料请关注脚本之家其它相关文章!
X 关闭
X 关闭
- 15G资费不大降!三大运营商谁提供的5G网速最快?中国信通院给出答案
- 2联想拯救者Y70发布最新预告:售价2970元起 迄今最便宜的骁龙8+旗舰
- 3亚马逊开始大规模推广掌纹支付技术 顾客可使用“挥手付”结账
- 4现代和起亚上半年出口20万辆新能源汽车同比增长30.6%
- 5如何让居民5分钟使用到各种设施?沙特“线性城市”来了
- 6AMD实现连续8个季度的增长 季度营收首次突破60亿美元利润更是翻倍
- 7转转集团发布2022年二季度手机行情报告:二手市场“飘香”
- 8充电宝100Wh等于多少毫安?铁路旅客禁止、限制携带和托运物品目录
- 9好消息!京东与腾讯续签三年战略合作协议 加强技术创新与供应链服务
- 10名创优品拟通过香港IPO全球发售4100万股 全球发售所得款项有什么用处?