产业互联网平台,淘宝客网站做seo,电子商务工资多少钱一个月,seo 重庆文章目录 1. 写在前面2. SheddingHandler的实现原理3. 相关方案的对比4. 小结 1. 写在前面
最近在看相关的Go服务的请求调度的时候#xff0c;发现在gin中默认提供的中间件中#xff0c;不含有请求调度相关的逻辑中间件#xff0c;去github查看了一些服务框架#xff0c;发… 文章目录 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.avgFlyingas.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反之为falsereturn 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过载上一次过载过了冷却器这个请求可以继续执行设置为falsereturn false
}可以看到请求被drop的前置条件有两个
系统的CPU负载超出了设定值目前go-zero设置的默认值为90%即系统CPU负载达到90%后就意味着系统过载了只要是过载请求会被直接拒绝否则判断第二个条件因为过载可能会随着流量减小而恢复或者丢弃的请求太多系统CPU会慢慢的恢复正常水平90%以下所以需要看一下过载时间如果超过了冷却时间而第一个条件又表示系统CPU负载正常此时我们会认定系统恢复了这个请求可以处理。
满足上述任一条件此请求就会进入最后的highThru()方法判断环节如果满足了此请求就会被丢弃。
从上面我们可以得到我们判断服务是否过载是依靠CPU的使用率去判断的那么我们如何动态的计算CPU的使用率呢
在go-zero里面采用的是直接获取linux机器上的cpu的相关文件然后通过代码逻辑将相关的文件进行解析并计算出CPU使用率。可以参考[cgroup_linux.go]
这里为了效率问题并不是实时去计算的而是在启动的时候启动了一个goroutine每250ms进行以此CPU使用率数据的刷新。
const (// 250ms and 0.95 as beta will count the average cpu load for past 5 secondscpuRefreshInterval time.Millisecond * 250allRefreshInterval time.Minute// moving average beta hyperparameterbeta 0.95
)var cpuUsage int64func 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请求被服务处理完后会减1if delta 0 {as.avgFlyingLock.Lock()// 平均请求数计算为当前平均请求数*0.9 当前运行请求数*0.1as.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 1as.passCounter.Reduce(func(b *collection.Bucket) {if b.Sum result {result b.Sum}})return int64(result)
}func (as *adaptiveShedder) minRt() float64 {result : defaultMinRtas.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文件的分析在某种程度上这种方案也是可行的不禁感叹自己还是太弱了还是要多学习加油