Go 模块在下游服务抖动恢复后CPU占用无法恢复原因

目录
  • 引言
  • 优先复用
  • 创建 g
  • allgs 在什么地方会用到

引言

某团圆节日公司服务到达历史峰值 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(&amp;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(&amp;allglock)
	for i := 0; i &lt; 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占用无法恢复原因的资料请关注我们其它相关文章!

时间: 2022-11-12

linux下通过go语言获得系统进程cpu使用情况的方法

本文实例讲述了linux下通过go语言获得系统进程cpu使用情况的方法.分享给大家供大家参考.具体分析如下: 这段代码通过linux的系统命令 ps来分析cpu的使用情况,代码如下: 复制代码 代码如下: package main import (     "bytes"     "log"     "os/exec"     "strconv"     "strings" ) type Process s

MongoDb CPU利用率过高问题如何解决

在公司的项目中,突然出现过一个情况,mongodb 的CPU利用率到达100%,导致服务器这边卡死了,请求了半天无响应,提示请求超时. 因为,当时APP用户可能会在某一个时间段集中的使用,所以,请求量一下子就飙上去了,刚好APP打开请求的时候,有一个mongodb的请求. 当时因为Mongodb的服务器不在我们这边,所以一下子没反应过来,不过最后还是给排除出,并解决了.这里就来记录下排查和解决的全过程. 问题分析: 1.根据代码,定位到了是Mongodb的报错. 2.进入Mongodb 服务器的

解决正则表示式匹配($regex)引起的一次mongo数据库cpu占用率高的问题

某一天,监控到mongo数据库cpu使用率高了很多,查了一下,发现是下面这种语句引起的: db.example_collection.find({ "idField" : { "$regex" : "123456789012345678" } , "dateField" : { "$regex" : "2019/10/10" }}) 通常,遇到这种情况,我第一反应是缺少相关字段的索引,导

Golang如何编写内存高效及CPU调优的Go结构体

目录 前言 输出结果 输出结果 前言 结构体是包含多个字段的集合类型,用于将数据组合为记录.这样可以将与同一实体相关联的数据利落地封装到一个轻量的类型定义中,然后通过对该结构体类型定义方法来实现不同的行为. 本文会尝试从内存利用和CPU周期的角度讲解如何高效编写struct. 我们来看下面这一结构体,这是我们一个奇怪用例所定义的terraform资源类型: type TerraformResource struct { Cloud string // 16字节 Name string // 16

golang通过node_exporter监控GPU及cpu频率、温度的代码

导语:通过node_exporter监控GPU以及cpu频率.温度,不想用一个node_exporter再加一个dcgm,分开监控.我这里监控的是热区的温度.如果需要监控各个cpu核心的温度需要修改一下代码. 结合了https://gitee.com/kevinliu_CQ/node_exporter监控GPU的代码. 加入了cpu的2项自定义监控https://gitee.com/jiaminxu/self_node_exporter 安装一下go wget https://dl.google

golang中定时器cpu使用率高的现象详析

前言: 废话少说,上线一个用golang写的高频的任务派发系统,上线跑着很稳定,但有个缺点就是当没有任务的时候,cpu的消耗也在几个百分点. 平均值在3%左右的cpu使用率.你没有任务的时候,cpu还跑到3%,这个说不过去呀.通过查看进程pidstat捕获得知,system系统的cpu消耗也不少. sys的cpu占用率高一般是由于大量的syscall系统调用引起的-. 下面的截图是用strace统计出来的系统调用-. 我们发现  futex 和 pselect6 的syscall非常的多-. 

在NET Core 中获取 CPU 使用率

以下文章来源于微信公众号DotNetCore实战  在 .NET Framework 中,很多人会用 PerformanceCounter 类做这件事情, 如下代码:     public class Program     {         public static void Main(string[] args)         {             while (true)             {                 var cpuUsage = GetCpuUs

golang&nbsp;一次性定时器Timer用法及实现原理详解

目录 前言 Timer timer结构体 创建定时器 停止定时器 重置定时器 实现原理 数据结构 runtimeTimer 创建Timer 停止Timer 重置Timer 前言 定时器在Go语言应用中使用非常广泛,Go语言的标准库里提供两种类型的计时器,一种是一次性的定时器Timer,另外一种是周期性的定时器Ticker.本文主要来看一下Timer的用法和实现原理,需要的朋友可以参考以下内容,希望对大家有帮助. Timer Timer是一种单一事件的定时器,即经过指定的时间后触发一个事件,因为T

Golang中的Slice与数组及区别详解

在golang中有数组和Slice两种数据结构,Slice是基于数组的实现,是长度动态不固定的数据结构,本质上是一个对数组字序列的引用,提供了对数组的轻量级访问.那么我们今天就给大家详细介绍下Golang中的Slice与数组, 1.Golang中的数组 数组是一种具有固定长度的基本数据结构,在golang中与C语言一样数组一旦创建了它的长度就不允许改变,数组的空余位置用0填补,不允许数组越界. 数组的一些基本操作:      1.创建数组: func main() { var arr1 = [.

golang中make和new的区别示例详解

前言 本文主要给大家介绍了关于golang中make和new区别的相关内容,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍: new 和 make 都可以用来分配空间,初始化类型,但是它们确有不同. new(T) 返回的是 T 的指针 new(T) 为一个 T 类型新值分配空间并将此空间初始化为 T 的零值,返回的是新值的地址,也就是 T 类型的指针 *T,该指针指向 T 的新分配的零值. p1 := new(int) fmt.Printf("p1 --> %#v \n &quo

golang中为什么Response.Body需要被关闭详解

前言 本文主要介绍了关于golang中Response.Body需要被关闭的相关内容,文中通过示例代码介绍的非常详细,对各位学习或者使用golang具有一定参考学习价值,下面话不多说了,来一起看看详细的介绍吧 Body io.ReadCloser The http Client and Transport guarantee that Body is always non-nil, even on responses without a body or responses with a zero

Golang中定时器的陷阱详解

前言 在业务中,我们经常需要基于定时任务来触发来实现各种功能.比如TTL会话管理.锁.定时任务(闹钟)或更复杂的状态切换等等.百纳网主要给大家介绍了关于Golang定时器陷阱的相关内容,所谓陷阱,就是它不是你认为的那样,这种认知误差可能让你的软件留下隐藏Bug.刚好Timer就有3个陷阱,我们会讲 1)Reset的陷阱和 2)通道的陷阱, 3)Stop的陷阱与Reset的陷阱类似,自己探索吧. 下面话不多说了,来一起看看详细的介绍吧 Reset的陷阱在哪 Timer.Reset()函数的返回值是

Golang中Set类型的实现方法示例详解

前言 本篇主要给大家讲述了如何利用Go语言的语法特性实现Set类型的数据结构,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍吧. 需求 对于Set类型的数据结构,其实本质上跟List没什么多大的区别.无非是Set不能含有重复的Item的特性,Set有初始化.Add.Clear.Remove.Contains等操作.接下来看具体的实现方式分析吧. 实现 仍然按照已有的编程经验来联想如何实现基本Set功能,在Java中很容易知道HashSet的底层实现是HashMap,核心的就是用一个常量

Javascript中click与blur事件的顺序详析

一.现象 最近在开发中碰到了一个需求,具体需求如下图. 这是一个很常见的需求,input框负责在点击回车和失焦的时候确认输入.button负责清除输入,input绑定代码为: input.addEventListener('blur',function(){ console.log('input blur'); }); input.addEventListener('keyup',function(){ console.log('input keyup'); }); "X"绑定的代码为

C++中4种强制类型转换的区别详析

前言 C++即支持C风格的类型转换,又有自己风格的类型转换.C风格的转换格式很简单,但是有不少缺点的: 1.转换太过随意,可以在任意类型之间转换.你可以把一个指向const对象的指针转换成指向非const对象的指针,把一个指向基类对象的指针转换成一个派生类对象的指针,这些转换之间的差距是非常巨大的,但是传统的C语言风格的类型转换没有区分这些. 2.C风格的转换没有统一的关键字和标示符.对于大型系统,做代码排查时容易遗漏和忽略. C++风格完美的解决了上面两个问题.1.对类型转换做了细分,提供了四