应对过载- go-zero源码阅读
运维一个可靠的系统 一个根本要就是能够优雅的处理过载的情况。
CPU资源的不足导致的负载上升是我们工作中最常见的,如果CPU资源不足以应对请求负载,一般来说所有的请求都会变慢,CPU负载过高会造成一系列的副作用,主要包括以下几项:
-
正在处理的(in-flight) 的请求数量上升
-
服务器逐渐将请求队列填满,意味着延迟上升,同时队列会用更多的内存
-
线程卡住,无法处理请求
-
cpu死锁或者请求卡主
-
rpc服务调用超时
-
cpu的缓存效率下降
应对服务器过载几种常见的策略:
- 服务降级
返回精度降低的回复,或者省略回复中需要经过大量计算的数据;
例如 :平时整个文档中进行搜索,在过载的情况下只搜索文档中的一小部分 - 服务端拒绝请求
- 客户端主动拒绝请求
一 资源利用率信号
在多数情况下,资源利用率仅仅是只当前cpu的消耗程度,在某些情况下也会考虑内存的使用情况。
当某个请求突然大批短时请求时,会导致负载的短时急剧上升。可以使用指数型衰变算法(exponential dacay)来平滑这个值。
go-zero案例:
go-zero种cpu的统计代码位于core/stat/internal中,通过获取linux系统的中/proc/stat提供的就是系统的CPU等的任务统计信息来计算cpu的使用率。
cpu统计代码会 没间隔250ms获取一次cpu的使用率,并且每一分钟打印一次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
)
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()
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:
printUsage()
}
}
}()
}
二 服务端过载保护
go-zero中内置的过载保护代码位于/core/load/adaptiveshedder.go
sholdDrop用来检测是否符合触发过载保护条件,代码入下
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
}
as.systemOverloaded() 方法判断当前cpu是否达到阈值,默认90%。
as.stillHot() 判断是否在冷却期。
as.highThru()判断仍是高吞吐。
如果触发过载保护,会返回过载错误信息,并记录错误日志。