某团圆节日公司服务到达历史峰值 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 的引用代码,发现该数组创建之后,并不会收缩。

我们可以根据上面看到的所有代码,来还原这种抖动情况下整个系统的情况了:

  1. 下游系统超时,很多 g 都被阻塞了,挂在 gopark 上,相当于提高了系统的并发

  2. 因为 gFree 没法复用,导致创建了比平时更多的 goroutine(具体有多少,就看你超时设置了多少

  3. 抖动时创建的 goroutine 会进入全局 allgs 数组,该数组不会进行收缩,且每次 gc、sysmon、死锁检查期间都会进行全局扫描

  4. 上述全局扫描导致我们的系统在下游系统抖动恢复之后,依然要去扫描这些抖动时创建的 g 对象,使 cpu 占用升高,idle 降低。

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

看起来并没有什么解决办法,如果想要复现这个问题的读者,可以试一下下面这个程序:

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 占用无法恢复相关推荐

  1. 服务和控制器应用CPU占用过高问题

    服务和控制器应用CPU占用过高问题 解决方案: 重启动 Windows Management Instrumentation 服务 这个治标不治本,只能解决当前开机至今的状态,下次开机问题仍然存在 不 ...

  2. win10服务和控制器应用CPU占用过高

    来源. RT,怎么办呢, 上面来源里说了可以通过重启动 Windows Management Instrumentation 服务解决 并且讲了,可以通过编写bat文件加入开机启动项. 但是,每个人的 ...

  3. 服务和控制器应用CPU占用过高 根源问题彻底解决

    病因: 造成这个问题并不是WIN10本身的问题,而是系统安装了一些服务性的软件造成了:他们往往依托服务Winmgmt(也就是Windows Management Instrumentation,即我们 ...

  4. 几个预防并发搞垮下游服务的方法

    前言 上一篇文章 我用休眠做并发控制,搞垮了下游服务 发出去后得到不少网友的回应,有人问自己平时用的方案行不行,有人建议借鉴TCP的拥塞控制策略,动态地调整发起的并发数,还有人问为啥我要管下游抗不抗得 ...

  5. 上游服务不可用了,下游服务如何应对?

    1. 引言 在系统中,上游服务和下游服务是两个关键概念.上游服务通常指的是提供某种功能或数据的服务端,它接收来自下游服务的请求,并根据请求进行处理和响应.下游服务通常指的是发起请求并依赖上游服务的客户 ...

  6. 我用休眠做并发控制,搞垮了下游服务

    今天通过一个因为并发控制不当导致下游服务崩溃的案例,给大家分享一个关于并发控制的误区. Go语言原生支持并发,只要使用go关键字就能把函数交给goroutine来并发地执行一段程序,正是因为并发难度特 ...

  7. 基于emq x开源版实现服务重启后主题和消息恢复的完整方案(二)

    emqx_restart_resume 用于emqx开源版 服务重启后恢复原订阅主题和持久化数据 问题 开源版emq在服务重启后原订阅的主题会清空,在客户端保持原clientId,保持原session ...

  8. Service Mesh(服务网格)——后 Kubernetes 时代的微服务

    本文转载自:宋净超的博客 这不是一篇教程,本文试图带您梳理清楚 Kubernetes.Envoy(xDS 协议)以及 Istio Service Mesh 之间的关系及内在联系.本文介绍了 Kuber ...

  9. mysql 数据库 xtrabackup (完全备份恢复,恢复后重启失败总结)

    一. 完全备份恢复说明 xtrabackup二进制文件有一个xtrabackup --copy-back选项,它将备份复制到服务器的datadir目录下.下面是通过 --target-dir 指定完全 ...

最新文章

  1. 01ts简介和相关配置
  2. java oom dump_Java OOM 内存溢出分析
  3. log4j.properties log4j.xml 路径问题
  4. linux系统嵌入式编译环境,Ubuntu 12.04嵌入式交叉编译环境arm-linux-gcc搭建过程图解...
  5. 0,1,2 代表标准输入、标准输出、标准错误
  6. 私有private成员和保护protected成员的区别
  7. ueditor富文本编辑器过滤了代码,如何取消?
  8. 当文科生遇见R语言,照样玩得转
  9. java 先进先出 集合_Java中的集合Queue、LinkedList、PriorityQueue(四)
  10. java零基础到精通全套视频教程
  11. 记七彩虹刷bios问题
  12. 《SVN web管理工具CollabNetSubversionEdge》
  13. Yolov1 + Yolov2 + Yolov3 发展史、论文、代码最全资源分享合集 ! ! !
  14. Grasshopper 二次开发 (C#) Part 1 - Introductions to the C# Coding in Grasshopper
  15. WPF 制作带TreeView的ComBox
  16. Java EasyPoi简单报表+复合表头报表的导出
  17. Java与es8实战之二:Springboot集成es8的Java Client
  18. linux centos 中的挂载是什么意思介绍和解释介绍及如何挂载
  19. 欢迎进入“健康之家”:Delos推出全球首个住宅健康技术平台
  20. 用 LaTeX 写漂亮学位论文(from wloo)

热门文章

  1. 搭建卷积神经网络怎么确定参数_AI入门:卷积神经网络
  2. Lindström–Gessel–Viennot lemma
  3. C++ Tricks
  4. oracle学习篇一:sqlplus常用命令
  5. 先序,中序,后序线索二叉树
  6. WPF学习12:基于MVVM Light 制作图形编辑工具(3)
  7. Java转iOS-第一个项目总结(2)
  8. GZFramwork快速开发框架演练之会员系统(四)添加商品管理
  9. 八部委宰割C类电商 消费者必遭殃
  10. 2021牛客多校2 - Girlfriend(球体积交)