在微服务中因为服务间相互依赖很容易呈现连锁故障,连锁故障可能是因为整个服务链路中的某一个服务呈现故障,进而导致系统的其余局部也呈现故障。例如某个服务的某个实例因为过载呈现故障,导致其余实例负载升高,从而导致这些实例像多米诺骨牌一样一个个全副呈现故障,这种连锁故障就是所谓的雪崩景象

比方,服务A依赖服务C,服务C依赖服务D,服务D依赖服务E,当服务E过载会导致响应工夫变慢甚至服务不可用,这个时候调用方D会呈现大量超时连贯资源被大量占用得不到开释,进而资源被耗尽导致服务D也过载,从而导致服务C过载以及整个零碎雪崩

<img src="https://gitee.com/kevwan/static/raw/master/doc/images/service_dependency.png" alt="service_dependency" style="zoom: 80%;" />

某一种资源的耗尽能够导致高提早、高错误率或者相应数据不合乎预期的状况产生,这些确实是在资源耗尽时应该呈现的状况,在负载一直回升直到过载时,服务器不可能始终放弃齐全的失常。而CPU资源的有余导致的负载回升是咱们工作中最常见的,如果CPU资源不足以应答申请负载,一般来说所有的申请都会变慢,CPU负载过高会造成一系列的副作用,次要包含以下几项:

  • 正在解决的(in-flight) 的申请数量回升
  • 服务器逐步将申请队列填满,意味着提早回升,同时队列会用更多的内存
  • 线程卡住,无奈解决申请
  • cpu死锁或者申请卡主
  • rpc服务调用超时
  • cpu的缓存效率降落

由此可见避免服务器过载的重要性显而易见,而避免服务器过载又分为上面几种常见的策略:

  • 提供降级后果
  • 在过载状况下被动拒绝请求
  • 调用方被动拒绝请求
  • 提前进行压测以及正当的容量布局

明天咱们次要探讨的是第二种避免服务器过载的计划,即在过载的状况下被动拒绝请求,上面我对立应用”过载爱护“来表述,过载爱护的大抵原理是当探测到服务器曾经处于过载时则被动拒绝请求不进行解决,个别做法是疾速返回error

<img src="https://gitee.com/kevwan/static/raw/master/doc/images/fail_fast.png" alt="fail_fast" style="zoom: 50%;" />

很多微服务框架中都内置了过载爱护能力,本文次要剖析go-zero中的过载爱护性能,咱们先通过一个例子来感触下go-zero的中的过载爱护是怎么工作的

首先,咱们应用官网举荐的goctl生成一个api服务和一个rpc服务,生成服务的过程比较简单,在此就不做介绍,能够参考官网文档,我的环境是两台服务器,api服务跑在本机,rpc服务跑在近程服务器

近程服务器为单核CPU,首先通过压力工具模仿服务器负载升高,把CPU打满

stress -c 1 -t 1000

此时通过uptime工具查看服务器负载状况,-d参数能够高亮负载的变动状况,此时的负载曾经大于CPU核数,阐明服务器正处于过载状态

watch -d uptime19:47:45 up 5 days, 21:55,  3 users,  load average: 1.26, 1.31, 1.44

此时申请api服务,其中ap服务外部依赖rpc服务,查看rpc服务的日志,级别为stat,能够看到cpu是比拟高的

"level":"stat","content":"(rpc) shedding_stat [1m], cpu: 986, total: 4, pass: 2, drop: 2"

并且会打印过载爱护抛弃申请的日志,能够看到过载爱护曾经失效,被动丢去了申请

adaptiveshedder.go:185 dropreq, cpu: 990, maxPass: 87, minRt: 1.00, hot: true, flying: 2, avgFlying: 2.07

这个时候调用方会收到 "service overloaded" 的报错

通过下面的试验咱们能够看到当服务器负载过高就会触发过载爱护,从而防止连锁故障导致雪崩,接下来咱们从源码来剖析下过载爱护的原理,go-zero在http和rpc框架中都内置了过载爱护性能,代码门路别离在go-zero/rest/handler/sheddinghandler.go和go-zero/zrpc/internal/serverinterceptors/sheddinginterceptor.go上面,咱们就以rpc上面的过载爱护进行剖析,在server启动的时候回new一个shedder 代码门路: go-zero/zrpc/server.go:119, 而后当收到每个申请都会通过Allow办法判断是否须要进行过载爱护,如果err不等于nil阐明须要过载爱护则间接返回error

promise, err = shedder.Allow()if err != nil {  metrics.AddDrop()  sheddingStat.IncrementDrop()  return}

实现过载爱护的代码门路为: go-zero/core/load/adaptiveshedder.go,这里实现的过载爱护基于滑动窗口能够避免毛刺,有冷却工夫避免抖动,当CPU>90%的时候开始拒绝请求,Allow的实现如下

func (as *adaptiveShedder) Allow() (Promise, error) {    if as.shouldDrop() {        as.dropTime.Set(timex.Now())        as.droppedRecently.Set(true)        return nil, ErrServiceOverloaded  // 返回过载谬误    }    as.addFlying(1) // flying +1    return &promise{        start:   timex.Now(),        shedder: as,    }, nil}

sholdDrop实现如下,该函数用来检测是否合乎触发过载爱护条件,如果合乎的话会记录error日志

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}

判断CPU是否达到预设值,默认90%

systemOverloadChecker = func(cpuThreshold int64) bool {    return stat.CpuUsage() >= cpuThreshold}

CPU的负载统计代码如下,每隔250ms会进行一次统计,每一分钟没记录一次统计日志

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()            }        }    }()}

其中CPU统计实现的代码门路为: go-zero/core/stat/internal,在该门路下应用linux结尾的文件,因为在go语言中会依据不同的零碎编译不同的文件,当为linux零碎时会编译以linux为后缀的文件

func init() {    cpus, err := perCpuUsage()    if err != nil {        logx.Error(err)        return    }    cores = uint64(len(cpus))    sets, err := cpuSets()    if err != nil {        logx.Error(err)        return    }    quota = float64(len(sets))    cq, err := cpuQuota()    if err == nil {        if cq != -1 {            period, err := cpuPeriod()            if err != nil {                logx.Error(err)                return            }            limit := float64(cq) / float64(period)            if limit < quota {                quota = limit            }        }    }    preSystem, err = systemCpuUsage()    if err != nil {        logx.Error(err)        return    }    preTotal, err = totalCpuUsage()    if err != nil {        logx.Error(err)        return    }}

在linux中,通过/proc虚构文件系统向用户控件提供了零碎外部状态的信息,而/proc/stat提供的就是零碎的CPU等的工作统计信息,这里次要原理就是通过/proc/stat来计算CPU的使用率

本文次要介绍了过载爱护的原理,以及通过试验触发了过载爱护,最初剖析了实现过载爱护性能的代码,置信通过本文大家对过载爱护会有进一步的意识,过载爱护不是万金油,对服务来说是有损的,所以在服务上线前咱们最好是进行压测做好资源布局,尽量避免服务过载

写作不易,如果感觉文章不错,欢送 github star ????

我的项目地址:https://github.com/tal-tech/go-zero

我的项目地址:
https://github.com/tal-tech/go-zero