Golang服务的请求调度

news2024/11/24 20:38:13

文章目录

  • 1. 写在前面
  • 2. SheddingHandler的实现原理
  • 3. 相关方案的对比
  • 4. 小结

1. 写在前面

最近在看相关的Go服务的请求调度的时候,发现在gin中默认提供的中间件中,不含有请求调度相关的逻辑中间件,去github查看了一些服务框架,发现在go-zero中,有一个SheddingHandler的中间件来帮助服务请求进行调度,防止在流量徒增的时候,服务出现滚雪球进一步恶化,导致最后服务不可用的现象出现。

SheddingHandler中间件存在的意义就是尽量保证服务可用的情况下尽可能多的处理请求,而在流量突增的时候,丢弃部分请求以确保服务可用,防止服务因为流量过大而崩溃。

2. SheddingHandler的实现原理

SheddingHandler简单来说就是维持了一套指标,在每个请求进入系统的时候,利用指标进行计算,判断当前的请求是否允许被进入系统,如果允许则请求通过中间件继续向下被服务处理,如果不被允许则在中间件层面就丢弃掉(正是这个丢弃,保证了在流量突增时服务的稳定)。

具体看源码:

// SheddingHandler returns a middleware that does load shedding.
func SheddingHandler(shedder load.Shedder, metrics *stat.Metrics) func(http.Handler) http.Handler {
	if shedder == nil {
		return func(next http.Handler) http.Handler {
			return next
		}
	}

	ensureSheddingStat() // 负责每分钟打印shedding相关的数据

	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			sheddingStat.IncrementTotal()
			promise, err := shedder.Allow() // 判断是否允许此请求进入下一步
			if err != nil {
				metrics.AddDrop() // drop掉请求,在中间件层面就拒绝了请求
				sheddingStat.IncrementDrop()
				logx.Errorf("[http] dropped, %s - %s - %s",
					r.RequestURI, httpx.GetRemoteAddr(r), r.UserAgent())
				w.WriteHeader(http.StatusServiceUnavailable)// 返回503,提示服务不可用
				return
			}

			cw := response.NewWithCodeResponseWriter(w)
			defer func() {
				if cw.Code == http.StatusServiceUnavailable {
					promise.Fail() // 相关指标记录
				} else {
					sheddingStat.IncrementPass()
					promise.Pass() // 相关指标记录
				}
			}()
			next.ServeHTTP(cw, r)
		})
	}
}

可以看到请求是否可以继续向下,取决于Allow()这个方法,这个方法的实现如下:

// Allow implements Shedder.Allow.
func (as *adaptiveShedder) Allow() (Promise, error) {
	if as.shouldDrop() {// 判断是否应该丢弃
		as.droppedRecently.Set(true)

		return nil, ErrServiceOverloaded// 丢弃
	}

	as.addFlying(1) // 通过校验

	return &promise{
		start:   timex.Now(),
		shedder: as,
	}, nil
}

继续看shouldDrop()方法:

func (as *adaptiveShedder) shouldDrop() bool {
	if as.systemOverloaded() || as.stillHot() {// 如果任一满足,这个请求都会被过载
		if as.highThru() {
			flying := atomic.LoadInt64(&as.flying)
			as.avgFlyingLock.Lock()
			avgFlying := as.avgFlying
			as.avgFlyingLock.Unlock()
			msg := fmt.Sprintf(
				"dropreq, cpu: %d, maxPass: %d, minRt: %.2f, hot: %t, flying: %d, avgFlying: %.2f",
				stat.CpuUsage(), as.maxPass(), as.minRt(), as.stillHot(), flying, avgFlying)
			logx.Error(msg)
			stat.Report(msg)
			return true
		}
	}

	return false
}

func (as *adaptiveShedder) systemOverloaded() bool {
	if !systemOverloadChecker(as.cpuThreshold) { // 校验CPU的负载是否超出设定值
		return false
	}

	as.overloadTime.Set(timex.Now())// 超出设定值,记录当前的时间(这主要是为了后续流量减小,系统的恢复用)
	return true
}

func (as *adaptiveShedder) stillHot() bool {
	if !as.droppedRecently.True() {// 如果这个请求之前有请求被drop这里值为true,反之为false
		return false// 之前的请求没有被drop表示系统可能没有遇到过载的问题,返回false
	}

	overloadTime := as.overloadTime.Load()// 如果之前有请求被drop,表示存在过载
	if overloadTime == 0 {// 看看是否有记录过载的时间
		return false
	}

	if timex.Since(overloadTime) < coolOffDuration {// 如果小于冷却时间,表示系统依然是过载状态
		return true
	}

	as.droppedRecently.Set(false)// 表示CPU过载,上一次过载过了冷却器,这个请求可以继续执行,设置为false
	return false
}

可以看到请求被drop的前置条件有两个:

  1. 系统的CPU负载超出了设定值,目前go-zero设置的默认值为90%,即系统CPU负载达到90%后,就意味着系统过载了,只要是过载,请求会被直接拒绝;否则判断第二个条件
  2. 因为过载可能会随着流量减小而恢复,或者丢弃的请求太多,系统CPU会慢慢的恢复正常水平(90%以下),所以需要看一下过载时间,如果超过了冷却时间,而第一个条件又表示系统CPU负载正常,此时我们会认定系统恢复了,这个请求可以处理。

满足上述任一条件,此请求就会进入最后的highThru()方法判断环节,如果满足了,此请求就会被丢弃。

从上面我们可以得到,我们判断服务是否过载,是依靠CPU的使用率去判断的,那么我们如何动态的计算CPU的使用率呢?

在go-zero里面,采用的是直接获取linux机器上的cpu的相关文件,然后通过代码逻辑将相关的文件进行解析并计算出CPU使用率。可以参考:[cgroup_linux.go]
linux cgroup信息

这里为了效率问题,并不是实时去计算的,而是在启动的时候,启动了一个goroutine每250ms进行以此CPU使用率数据的刷新。

const (
	// 250ms and 0.95 as beta will count the average cpu load for past 5 seconds
	cpuRefreshInterval = time.Millisecond * 250
	allRefreshInterval = time.Minute
	// moving average beta hyperparameter
	beta = 0.95
)

var cpuUsage int64

func init() {
	go func() {
		cpuTicker := time.NewTicker(cpuRefreshInterval)
		defer cpuTicker.Stop()
		allTicker := time.NewTicker(allRefreshInterval)
		defer allTicker.Stop()

		for {
			select {
			case <-cpuTicker.C:
				threading.RunSafe(func() {
					curUsage := internal.RefreshCpu() // 刷新CPU使用率数据
					prevUsage := atomic.LoadInt64(&cpuUsage)
					// cpu = cpuᵗ⁻¹ * beta + cpuᵗ * (1 - beta)
					usage := int64(float64(prevUsage)*beta + float64(curUsage)*(1-beta))
					atomic.StoreInt64(&cpuUsage, usage)
				})
			case <-allTicker.C:
				if logEnabled.True() {
					printUsage()
				}
			}
		}
	}()
}

最后再来看highThru()方法,这个方法相对来说比较复杂:

func (as *adaptiveShedder) addFlying(delta int64) {
	flying := atomic.AddInt64(&as.flying, delta)// 请求通过检验进入后会加1,请求被服务处理完后会减1
	if delta < 0 {
		as.avgFlyingLock.Lock()
		// 平均请求数计算为当前平均请求数*0.9 + 当前运行请求数*0.1
		as.avgFlying = as.avgFlying*flyingBeta + float64(flying)*(1-flyingBeta)
		as.avgFlyingLock.Unlock()
	}
}

func (as *adaptiveShedder) highThru() bool {
	as.avgFlyingLock.Lock()
	avgFlying := as.avgFlying // 运行中的平均请求数
	as.avgFlyingLock.Unlock()
	maxFlight := as.maxFlight()// 运行的最大的请求数
	// 如果运行的平均请求数>最大的请求数且当前运行的请求数>最大的请求数,表示依旧高负载
	return int64(avgFlying) > maxFlight && atomic.LoadInt64(&as.flying) > maxFlight
}

func (as *adaptiveShedder) maxFlight() int64 {
	// windows = buckets per second
	// maxQPS = maxPASS * windows
	// minRT = min average response time in milliseconds
	// maxQPS * minRT / milliseconds_per_second
	// 最大的运行数的计算为最大请求数*窗口的长度*最小的处理时间
	return int64(math.Max(1, float64(as.maxPass()*as.windows)*(as.minRt()/1e3)))
}

上面关于flying的计算,在SheddingHandler中有两个count统计器在统计这通过的总请求数以及请求的平均耗时。默认会在5s的时间内启动50个大小的bucket来循环滚动,即每个bucket统计100ms内的请求数。

这里利用窗口统计请求数大小的判断主要是为了规避在负载的情况下,丢弃了太多的请求导致系统实际运行的请求数减少的太多,所以加了这一层判断,这个可以保证在系统高负载丢弃了大量的请求的情况下,系统尽可能多的处理更多的请求,而不是负载一高就直接丢弃。

func (as *adaptiveShedder) maxPass() int64 {
	var result float64 = 1

	as.passCounter.Reduce(func(b *collection.Bucket) {
		if b.Sum > result {
			result = b.Sum
		}
	})

	return int64(result)
}

func (as *adaptiveShedder) minRt() float64 {
	result := defaultMinRt

	as.rtCounter.Reduce(func(b *collection.Bucket) {
		if b.Count <= 0 {
			return
		}

		avg := math.Round(b.Sum / float64(b.Count))
		if avg < result {
			result = avg
		}
	})

	return result
}

3. 相关方案的对比

在调度请求这一块,go-zero的方案确实很棒,结合了CPU使用率和过载冷缺以及请求数大小因素,不仅保证了系统高负载下服务的正常,还确保了系统能够尽可能多的处理请求。

但从我们目前的调度模式以及执行单元的状态角度出发,我们会发现服务接收到一个请求后会解析请求读取请求的内容,然后调度此请求给到执行单元,这个执行单元可能是一个线程或者一个Goroutine,从执行单元的角度来看,以线程为例,线程的生命周期会有如下图所示的几个阶段:

  • 新建
  • 就绪
  • 运行
  • 阻塞
  • 死亡
    线程的生命周期

我们再从系统服务的限制方面考虑,一般系统的限制包括I/O限制和CPU限制,I/O限制指代I/O密集型的应用程序的限制,而CPU限制则是CPU密集型应用程序的限制:

  • I/O密集型:表示服务需要进行大量的I/O操作,如磁盘读写、网络传输等,这类服务不需要进行大量的计算,但需要等待I/O操作完成,所以一般CPU占用率很低。
  • CPU密集型:表示服务需要进行大量的CPU操作,如数据处理、图像处理、加密解密等,这类服务需要进行大量的计算,但不需要进行太多I/O相关的操作,所以I/O等待时间短,CPU占用率高。

在目前的服务应用中,绝大部分的应用程序是CPU密集型。

而CPU密集型服务,要想最大限度的利用CPU,最理想的情况所有的执行单元都处于运行和等待的状态,但等待和运行之间有个就绪的中间态,这也就意味着,如果想让所有的执行单元都处于运行和代码状态,我们就需要最小化就绪的执行单元数量。而就绪单元一旦获取到CPU资源(时间片)就会进入Running状态。

如果处于就绪的单元不断增多,在某种意义上意味着程序的CPU资源不足,即CPU过负载。从这个角度出发,我们可以利用执行单元处于就绪态的数量来判断服务是否过载。

在Golang的GMP模型中,P的数量是一定的,M的数量最多不超过10000个,而Goroutine的数量几乎是不定的。从上面利用就绪态(在Golang中是GRunnable状态)的数量来判断系统过载,也给我们提供了一个新的方案:判断系统所有P上(本地队列)的Goroutine处于GRunnable的数量,如果数量超过一个界定值,表示CPU资源不足,即过载。

4. 小结

在刚开始接触到服务的请求调度的时候,就想着看看是否有开源的方案来解决这个问题,果不其然,你能够想到的,大家曾经都想到过并付诸了时间和精力去给出了具体的方案设计,无论是SheddingHandler的设计,还是利用Goroutine的状态来判断系统是否过载,它们都有各自的理论为依托,但从精确度来说go-zero的SheddingHandler的设计相对来说更为准确,因为从CPU的真实数据出发,得到具体的CPU是否负载是最为可靠直观的。

判断Goroutine的就绪态数量这个方案,在最开始的接触中,自己是不太理解的,但从具体理论出发,包括后续自己也进行了相关的压测,以及Golang的trace.out文件的分析,在某种程度上,这种方案也是可行的,不禁感叹自己还是太弱了,还是要多学习,加油!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/873399.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

任我行 CRM SQL注入漏洞复现(HW0day)

0x01 产品简介 任我行CRM&#xff08;Customer Relationship Management&#xff09;是一款专业的企业级CRM软件&#xff0c;旨在帮助企业有效管理客户关系、提升销售效率和提供个性化的客户服务。 0x02 漏洞概述 任我行 CRM SmsDataList 接口处存在SQL注入漏洞&#xff0c;未…

2023HVV最新0day、1day消息含POC、EXP

点击"仙网攻城狮”关注我们哦~ 不想当研发的渗透人不是好运维 让我们每天进步一点点 简介 2023HW-8月10号0day、1day漏洞汇总&#xff0c;包含以下漏洞需要自取。 链接&#xff1a;https://pan.baidu.com/s/1duOyDNjYBPKfC5eB9ZHA2Q 提取码&#xff1a;6666 通达OA sql注入…

JavaWeb课程学习--Day01

HTML 建立css文件&#xff1a; css使用方式&#xff1a; <span>...</span>无语意包裹标签 css中的三种选择器&#xff1a; 注意&#xff1a;播放视音频时要留出播放空间 盒子模型&#xff1a; 表格标签&#xff1a; 以上表格&#xff1a; 表单标签&#xff1a; 表…

form表单构造http请求的写法

from是html的一个标签&#xff0c;from是html后早http请求的一种方式&#xff0c;它和input标签密切配合。 from有两个基本属性action和method&#xff0c;action就是http请求url中的路径部分。method就是构造的http请求的方法。 form和input标签配合构造键值对&#xff0c;键值…

枚举缓存工具

此文章为笔记&#xff0c;为阅读其他文章的感受、补充、记录、练习、汇总&#xff0c;非原创&#xff0c;感谢每个知识分享者。 文章目录 1. 背景2. 枚举缓存3. 样例展示4. 性能对比5. 总结 本文通过几种样例展示如何高效优雅的使用java枚举消除冗余代码。 1. 背景 枚举在系统…

Android学习之路(4) UI控件之文本框

本节给大家带来的UI控件是&#xff1a;TextView(文本框)&#xff0c;用于显示文本的一个控件&#xff0c;另外声明一点&#xff0c;我不是翻译API文档&#xff0c;不会一个个属性的去扣&#xff0c;只学实际开发中常用的&#xff0c;有用的&#xff0c;大家遇到感觉到陌生的属性…

C++使用new来初始化指向类的指针

C使用new来初始化类的指针 1.ClassName * p new ClassName; 调用默认构造函数。 如果类里没有写默认构造函数&#xff0c;会使用编译器帮我们生成的&#xff0c;但不会初始化成员变量&#xff0c;如 class NoConstructor //没写构造函数的类 { public:~NoConstructor() …

golang—面试题大全

目录标题 sliceslice和array的区别slice扩容机制slice是否线程安全slice分配到栈上还是堆上扩容过程中是否重新写入go深拷贝发生在什么情况下&#xff1f;切片的深拷贝是怎么做的copy和左值进行初始化区别slice和map的区别 mapmap介绍map的key的类型map对象如何比较map的底层原…

T113-S3-RTL8211网口phy芯片调试

目录 前言 一、RTL8211介绍 二、硬件连接 三、设备树配置 四、内核配置 五、phy芯片配置 六、调试问题 总结 前言 在嵌入式系统开发中&#xff0c;网络连接是至关重要的一部分。T113-S3开发板搭载了RTL8211系列的网口PHY芯片&#xff0c;用于实现以太网连接。在开发过程…

[C语言] 指针

1. 指针是什么 2. 指针和指针类型 3. 野指针 4. 指针运算 5. 指针和数组 6. 二级指针 7. 指针数组 目录 1. 指针是什么&#xff1f; 2. 指针和指针类型 2.1 指针-整数 2.2 指针的解引用 3. 野指针 3.1 野指针成因 3.2 如何规避野指针 4. 指针运算 4.1 指针…

《Java-SE-第三十六章》之枚举

前言 在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!” 博客主页&#xff1a;KC老衲爱尼姑的博客主页 博主的github&#xff0c;平常所写代码皆在于此 共勉&#xff1a;talk is cheap, show me the code 作者是爪哇岛的新手&#xff0c;水平很有限&…

预知未来:揭示公司倒闭的隐秘迹象

引言 在商业世界中&#xff0c;公司的倒闭是一种常见的现象。然而&#xff0c;对于那些在公司中工作的人来说&#xff0c;这可能是一场灾难。作为一名资深的人力资源专业人员&#xff0c;我认为我们有责任提前察觉公司可能倒闭的迹象&#xff0c;以便我们可以采取适当的行动来…

一起学SF框架系列7.2-spring-AOP-AOP使用

Spring AOP有两种使用模式&#xff1a;AspectJ配置模式和xml配置模式。 AspectJ配置模式 配置 1、加入依赖包&#xff1a; <!--spring aop依赖--><dependency><groupId>org.springframework</groupId><artifactId>spring-aop</artifactId&…

Gartner发布2023年的存储技术成熟曲线

技术路线说明 Gartner自1995年起开始采用技术成熟度曲线&#xff0c;它描述创新的典型发展过程&#xff0c;即从过热期发展到幻灭低谷期&#xff0c;再到人们最终理解创新在市场或领域内的意义和角色。 一项技术 (或相关创新)在发展到最终成熟期的过程中经历多个阶段&#xff1…

接口也默认继承Object类

目标&#xff1a; 1. 知道接口也默认继承Object类 2. 感受继承和重写在向上转型中的作用有多大 引出问题&#xff1a; 在我用 List接口的引用list 接收ArrayList这个集合类的实例时&#xff08;此处发生向上转型&#xff09;&#xff0c;偶然发现&#xff0c;咦&#xff0c…

【Python常用函数】一文让你彻底掌握Python中的toad.selection.select函数

任何事情都是由量变到质变的过程&#xff0c;学习Python也不例外。只有把一个语言中的常用函数了如指掌了&#xff0c;才能在处理问题的过程中得心应手&#xff0c;快速地找到最优方案。本文和你一起来探索Python中的toad.selection.select函数&#xff0c;让你以最短的时间明白…

Grounding dino + segment anything + stable diffusion 实现图片编辑

目录 总体介绍总体流程 模块介绍目标检测&#xff1a; grounding dino目标分割&#xff1a;Segment Anything Model (SAM)整体思路模型结构&#xff1a;数据引擎 图片绘制 集成样例 其他问题附录 总体介绍 总体流程 本方案用到了三个步骤&#xff0c;按顺序依次为&#xff1a…

YOLOv5入门实践(2)— 手把手教你使用make sense标注数据集(附工具地址+使用教程)

前言&#xff1a;Hello大家好&#xff0c;我是小哥谈。鉴于labelimg图像标注软件安装起来比较麻烦&#xff0c;本节课再给大家介绍另外一款数据集标注工具。这是一款在线标注数据集的工具&#xff0c;用起来非常的方便。&#x1f308; 前期回顾&#xff1a; YOLOv5入门实践&…

当执行MOV [0001H] 01H指令时,CPU都做了什么?

今天和几位单位大佬聊天时&#xff0c;讨论到一个非常有趣的问题-当程序执行MOV [0001H], 01H计算机实际上都做了哪些工作&#xff1f;乍一看这个问题平平无奇&#xff0c;CPU只是把立即数01H放在了地址为0001的内存里&#xff0c;但仔细想想这个问题远没有那么简单&#xff0c…

SystemVerilog之覆盖率详解

文章目录 1.0 覆盖率前言1.1 覆盖率类型1.2 覆盖策略及覆盖组1.3 覆盖率数据采样1.3.1 bin的创建与使用1.3.2 条件覆盖率1.3.3 翻转覆盖率1.3.4 wildcard覆盖率1.3.5 忽略bin与非法bin 1.4 交叉覆盖率1.4.1 排除部分cross bin1.4.2 精细化交叉覆盖率1.4.3 单个实例的覆盖率1.4.…