乐趣区

关于golang:微服务过载保护原理与实战

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

比方,服务 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 uptime

19: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

退出移动版