Go可用性 限流 : 自适应限流

2024-04-23 03:18
文章标签 go 可用性 限流 适应

本文主要是介绍Go可用性 限流 : 自适应限流,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在前面限流的三篇文章,我们学习了令牌桶、漏桶算法的原理、实现以及使用方式,不知道你有没有觉得这两种算法存在着一些问题。

  • Go 可用性(二) 限流 1: 令牌桶原理及使用
  • Go 可用性(三) 限流 2: 令牌桶的实现 rate/limt
  • Go 可用性(四) 限流 3: 漏桶算法

这两种算法最大的一个问题就是他们都属于需要提前设置阈值的算法,基于 QPS 进行限流的时候最麻烦的就是这个阈值应该怎么设定。一般来说我们可以通过压测来决定这个阈值。

  • 但是如果每个系统上线前都要经过很严格的压测,那么成本相对来说会比较大
  • 并且我们很多时候压测都会在测试环境进行压测,测试环境一般来说和生产环境会有一定的差异,即使我们在生产环境做了压测,现在我们的应用都是以容器的形式跑在不同的宿主机上的,每台宿主机上的差异,以及不同的负载都会导致这个压测时的结果不一定就一定是正确的
  • 当我们的机器型号、数量等发生改变时,之前压测的指标能不能用其实是一个问题,这些数据对于系统负载的影响其实不是线性的,举个例子之前一台机器,后面再加一台,负载就一定能到 2 倍么?其实是不一定的
  • 如果需要修改限流的值,虽然之前我们将令牌桶的限流是可以动态调整,但是靠人去调整,如果真出现问题然后再叫运维或者是开发同学去调整可能黄花菜都凉了

既然这种方式有这么多的缺点,那有没有办法解决呢?答案就是今天讲到的 自适应限流

自适应限流

自适应限流怎么做

前面我们遇到的主要问题就是每个服务实例的限流阈值实际应该是动态变化的,我们应该根据系统能够承载的最大吞吐量,来进行限流,当当前的流量大于最大吞吐的时候就限制流量进入,反之则允许通过。那现在的问题就是

  • 系统的吞吐量该如何计算?
  • 什么时候系统的吞吐量就是最大的吞吐量了?

**

计算吞吐量:利特尔法则 L = λ * W

利特尔法则由麻省理工大学斯隆商学院(MIT Sloan School of Management)的教授 John Little﹐于 1961 年所提出与证明。它是一个有关提前期与在制品关系的简单数学公式,这一法则为精益生产的改善方向指明了道路。 —- MBA 智库百科 (mbalib.com)

如上图所示,如果我们开一个小店,平均每分钟进店 2 个客人(λ),每位客人从等待到完成交易需要 4 分钟(W),那我们店里能承载的客人数量就是 2 * 4 = 8 个人

同理,我们可以将 λ 当做 QPS, W 呢是每个请求需要花费的时间,那我们的系统的吞吐就是 L = λ * W ,所以我们可以使用利特尔法则来计算系统的吞吐量。

什么时候系统的吞吐量就是最大的吞吐量?

首先我们可以通过统计过去一段时间的数据,获取到平均每秒的请求量,也就是 QPS,以及请求的耗时时间,为了避免出现前面 900ms 一个请求都没有最后 100ms 请求特别多的情况,我们可以使用滑动窗口算法来进行统计。

最容易想到的就是我们从系统启动开始,就把这些值给保存下来,然后计算一个吞吐的最大值,用这个来表示我们的最大吞吐量就可以了。但是这样存在一个问题是,我们很多系统其实都不是独占一台机器的,一个物理机上面往往有很多服务,并且一般还存在一些超卖,所以可能第一个小时最大处理能力是 100,但是这台节点上其他服务实例同时都在抢占资源的时候,这个处理能力最多就只能到 80 了

所以我们需要一个数据来做启发阈值,只要这个指标达到了阈值那我们就进入流控当中。常见的选择一般是 CPU、Memory、System Load,这里我们以 CPU 为例

只要我们的 CPU 负载超过 80% 的时候,获取过去 5s 的最大吞吐数据,然后再统计当前系统中的请求数量,只要当前系统中的请求数大于最大吞吐那么我们就丢弃这个请求。

kratos 自适应限流分析

限流公式

// PS: 官方文档这里写的是 cpu > 800 AND (Now - PrevDrop) < 1s
// 应该是写错了,等下看源码就知道了
(cpu > 800 OR (Now - PrevDrop) < 1s) AND (MaxPass * MinRt * windows / 1000) < InFlight
  • cpu > 800 表示 CPU 负载大于 80% 进入限流
  • (Now - PrevDrop) < 1s 这个表示只要触发过 1 次限流,那么 1s 内都会去做限流的判定,这是为了避免反复出现限流恢复导致请求时间和系统负载产生大量毛刺
  • (MaxPass * MinRt * windows / 1000) < InFlight 判断当前负载是否大于最大负载
    InFlight
    (MaxPass * MinRt * windows / 1000)
    MaxPass
    MinRt
    windows

源码分析

BBR 结构体
type BBR struct {cpu             cpuGetter// 请求数,和响应时间的采样数据,使用滑动窗口进行统计passStat        metric.RollingCounterrtStat          metric.RollingCounter
</span><span class="com">// 当前系统中的请求数</span><span class="pln">
inFlight        int64
</span><span class="com">// 每秒钟内的采样数量,默认是10</span><span class="pln">
winBucketPerSec int64
</span><span class="com">// 单个 bucket 的时间</span><span class="pln">
bucketDuration  time</span><span class="pun">.</span><span class="typ">Duration</span><span class="pln">
</span><span class="com">// 窗口数量</span><span class="pln">
winSize         </span><span class="kwd">int</span><span class="pln">
</span><span class="com">// 配置</span><span class="pln">
conf            </span><span class="pun">*</span><span class="typ">Config</span><span class="pln">
prevDrop        atomic</span><span class="pun">.</span><span class="typ">Value</span><span class="pln">
</span><span class="com">// 表示最近 5s 内,单个采样窗口中最大的请求数的缓存数据</span><span class="pln">
maxPASSCache    atomic</span><span class="pun">.</span><span class="typ">Value</span><span class="pln">
</span><span class="com">// 表示最近 5s 内,单个采样窗口中最小的响应时间的缓存数据</span><span class="pln">
minRtCache      atomic</span><span class="pun">.</span><span class="typ">Value</span><span class="pln">

}

Allow: 判断请求是否允许通过
func (l *BBR) Allow(ctx context.Context, opts ...limit.AllowOption) (func(info limit.DoneInfo), error) {// ... 省略配置修改代码
</span><span class="kwd">if</span><span class="pln"> l</span><span class="pun">.</span><span class="pln">shouldDrop</span><span class="pun">()</span><span class="pln"> </span><span class="pun">{</span><span class="pln"></span><span class="kwd">return</span><span class="pln"> </span><span class="kwd">nil</span><span class="pun">,</span><span class="pln"> ecode</span><span class="pun">.</span><span class="typ">LimitExceed</span><span class="pln">
</span><span class="pun">}</span><span class="pln">atomic</span><span class="pun">.</span><span class="typ">AddInt64</span><span class="pun">(&amp;</span><span class="pln">l</span><span class="pun">.</span><span class="pln">inFlight</span><span class="pun">,</span><span class="pln"> </span><span class="lit">1</span><span class="pun">)</span><span class="pln">
stime </span><span class="pun">:=</span><span class="pln"> time</span><span class="pun">.</span><span class="typ">Since</span><span class="pun">(</span><span class="pln">initTime</span><span class="pun">)</span><span class="pln"></span><span class="kwd">return</span><span class="pln"> func</span><span class="pun">(</span><span class="kwd">do</span><span class="pln"> limit</span><span class="pun">.</span><span class="typ">DoneInfo</span><span class="pun">)</span><span class="pln"> </span><span class="pun">{</span><span class="pln">rt </span><span class="pun">:=</span><span class="pln"> int64</span><span class="pun">((</span><span class="pln">time</span><span class="pun">.</span><span class="typ">Since</span><span class="pun">(</span><span class="pln">initTime</span><span class="pun">)</span><span class="pln"> </span><span class="pun">-</span><span class="pln"> stime</span><span class="pun">)</span><span class="pln"> </span><span class="pun">/</span><span class="pln"> time</span><span class="pun">.</span><span class="typ">Millisecond</span><span class="pun">)</span><span class="pln">l</span><span class="pun">.</span><span class="pln">rtStat</span><span class="pun">.</span><span class="typ">Add</span><span class="pun">(</span><span class="pln">rt</span><span class="pun">)</span><span class="pln">atomic</span><span class="pun">.</span><span class="typ">AddInt64</span><span class="pun">(&amp;</span><span class="pln">l</span><span class="pun">.</span><span class="pln">inFlight</span><span class="pun">,</span><span class="pln"> </span><span class="pun">-</span><span class="lit">1</span><span class="pun">)</span><span class="pln"></span><span class="kwd">switch</span><span class="pln"> </span><span class="kwd">do</span><span class="pun">.</span><span class="typ">Op</span><span class="pln"> </span><span class="pun">{</span><span class="pln"></span><span class="kwd">case</span><span class="pln"> limit</span><span class="pun">.</span><span class="typ">Success</span><span class="pun">:</span><span class="pln">l</span><span class="pun">.</span><span class="pln">passStat</span><span class="pun">.</span><span class="typ">Add</span><span class="pun">(</span><span class="lit">1</span><span class="pun">)</span><span class="pln"></span><span class="kwd">return</span><span class="pln"></span><span class="kwd">default</span><span class="pun">:</span><span class="pln"></span><span class="kwd">return</span><span class="pln"></span><span class="pun">}</span><span class="pln">
</span><span class="pun">},</span><span class="pln"> </span><span class="kwd">nil</span><span class="pln">

}

这个方法主要是给中间件使用的

  • 首先使用 shouldDrop 方法判断这个请求是否应该丢弃
  • 如果成功放行,那么当前系统中的请求数就 +1
  • 然后返回一个 function 用于请求结束之后
    Inflight
shouldDrop: 判断请求是否应该被丢弃
func (l *BBR) shouldDrop() bool {if l.cpu() < l.conf.CPUThreshold {prevDrop, _ := l.prevDrop.Load().(time.Duration)if prevDrop == 0 {return false}if time.Since(initTime)-prevDrop <= time.Second {inFlight := atomic.LoadInt64(&l.inFlight)return inFlight > 1 && inFlight > l.maxFlight()}l.prevDrop.Store(time.Duration(0))return false}inFlight := atomic.LoadInt64(&l.inFlight)drop := inFlight > 1 && inFlight > l.maxFlight()if drop {prevDrop, _ := l.prevDrop.Load().(time.Duration)if prevDrop != 0 {return drop}l.prevDrop.Store(time.Since(initTime))}return drop
}

这个方法其实就是开头讲到的限流公式了,逻辑如下图所示

  • 首先看 CPU 的使用率是否达到了阈值
  • 如果没到,则回去判断一下上次触发限流到现在是否在一秒以内
    prevDrop
  • 如果到了,则判断一下当前负载是否超过限制
    prevDrop
maxFlight: 系统的最大负载
func (l *BBR) maxFlight() int64 {return int64(math.Floor(float64(l.maxPASS()*l.minRT()*l.winBucketPerSec)/1000.0 + 0.5))
}

这个就是计算过去一段时间系统的最大负载是多少

总结

这篇文章我们讲了一下为什么需要自适应限流,令牌桶和漏桶这类需要手动设置 rps 算法的问题所在,了解了自适应限流的实现原理,最后看了一下 kratos 当中是如何实现自适应限流的。但是由于篇幅关系,CPU 的数据如何进行统计,文章中提到了很多次的滑动窗口是个什么原理这些知识点大家可以自行查看 kratos 中的源码,或者去看极客时间的 Go 进阶训练营都有讲到。

kratos 中的限流算法其实是借鉴了 sentinel 的实现,只是 sentinel 默认使用 load 作为启发阈值,而 kratos 使用了 cpu,kratos 为什么要使用 cpu 呢?这个大家可以自己想想。

而 sentinel 的实现其实是参考了 TCP 中的 BBR 算法,在 BBR 的基础上加上了 load 作为启发阈值的判断,所以多了解一下基础知识总是没错的,指不定当下遇到的场景就能解决。

这篇关于Go可用性 限流 : 自适应限流的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/927643

相关文章

深入理解Go语言中二维切片的使用

《深入理解Go语言中二维切片的使用》本文深入讲解了Go语言中二维切片的概念与应用,用于表示矩阵、表格等二维数据结构,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧... 目录引言二维切片的基本概念定义创建二维切片二维切片的操作访问元素修改元素遍历二维切片二维切片的动态调整追加行动态

go中的时间处理过程

《go中的时间处理过程》:本文主要介绍go中的时间处理过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 获取当前时间2 获取当前时间戳3 获取当前时间的字符串格式4 相互转化4.1 时间戳转时间字符串 (int64 > string)4.2 时间字符串转时间

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

Go语言中nil判断的注意事项(最新推荐)

《Go语言中nil判断的注意事项(最新推荐)》本文给大家介绍Go语言中nil判断的注意事项,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录1.接口变量的特殊行为2.nil的合法类型3.nil值的实用行为4.自定义类型与nil5.反射判断nil6.函数返回的

Go语言数据库编程GORM 的基本使用详解

《Go语言数据库编程GORM的基本使用详解》GORM是Go语言流行的ORM框架,封装database/sql,支持自动迁移、关联、事务等,提供CRUD、条件查询、钩子函数、日志等功能,简化数据库操作... 目录一、安装与初始化1. 安装 GORM 及数据库驱动2. 建立数据库连接二、定义模型结构体三、自动迁

Go语言代码格式化的技巧分享

《Go语言代码格式化的技巧分享》在Go语言的开发过程中,代码格式化是一个看似细微却至关重要的环节,良好的代码格式化不仅能提升代码的可读性,还能促进团队协作,减少因代码风格差异引发的问题,Go在代码格式... 目录一、Go 语言代码格式化的重要性二、Go 语言代码格式化工具:gofmt 与 go fmt(一)

Spring Boot 实现 IP 限流的原理、实践与利弊解析

《SpringBoot实现IP限流的原理、实践与利弊解析》在SpringBoot中实现IP限流是一种简单而有效的方式来保障系统的稳定性和可用性,本文给大家介绍SpringBoot实现IP限... 目录一、引言二、IP 限流原理2.1 令牌桶算法2.2 漏桶算法三、使用场景3.1 防止恶意攻击3.2 控制资源

Go学习记录之runtime包深入解析

《Go学习记录之runtime包深入解析》Go语言runtime包管理运行时环境,涵盖goroutine调度、内存分配、垃圾回收、类型信息等核心功能,:本文主要介绍Go学习记录之runtime包的... 目录前言:一、runtime包内容学习1、作用:① Goroutine和并发控制:② 垃圾回收:③ 栈和

Go语言中泄漏缓冲区的问题解决

《Go语言中泄漏缓冲区的问题解决》缓冲区是一种常见的数据结构,常被用于在不同的并发单元之间传递数据,然而,若缓冲区使用不当,就可能引发泄漏缓冲区问题,本文就来介绍一下问题的解决,感兴趣的可以了解一下... 目录引言泄漏缓冲区的基本概念代码示例:泄漏缓冲区的产生项目场景:Web 服务器中的请求缓冲场景描述代码

Go语言如何判断两张图片的相似度

《Go语言如何判断两张图片的相似度》这篇文章主要为大家详细介绍了Go语言如何中实现判断两张图片的相似度的两种方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 在介绍技术细节前,我们先来看看图片对比在哪些场景下可以用得到:图片去重:自动删除重复图片,为存储空间"瘦身"。想象你是一个