新来个技能总监,把限流完结的那叫一个高雅,敬服!

我们好,我是楼仔!

在电商高并发场景下,我们常常会运用一些常用方法,去应对流量顶峰,比如限流、熔断、降级,今天我们聊聊限流。

什么是限流呢?限流是束缚抵达系统的并发央求数量,保证系统可以正常呼应部分用户央求,而关于超越束缚的流量,则通过拒绝服务的方法保证整系统统的可用性。

根据限流作用范围,可以分为单机限流和分布式限流;根据限流方法,又分为计数器、滑动窗口、漏桶限令牌桶限流,下面我们对这块具体进行讲解。

常用限流方法

计数器

计数器是一种最简略限流算法,其原理就是:在一段时间距离内,对央求进行计数,与阀值进行比较判别是否需求限流,一旦到了时间临界点,将计数器清零。

这个就像你去坐车相同,车厢规矩了多少个方位,满了就不让上车了,不然就是超载了,被交警叔叔抓到了就要罚款的,假设我们的系统那就不是罚款的工作了,或许直接崩掉了。

程序实行逻辑:

  • 可以在程序中设置一个变量 count,当过来一个央求我就将这个数 +1,一起记载央求时间。
  • 当下一个央求来的时分判别 count 的计数值是否超越设定的频次,以及其时央求的时间和第一次央求时间是否在 1 分钟内。
  • 假设在 1 分钟内而且超越设定的频次则证明央求过多,后边的央求就拒绝掉。
  • 假设该央求与第一个央求的距离时间大于计数周期,且 count 值还在限流范围内,就重置 count。

那么问题来了,假设有个需求关于某个接口 /query 每分钟最多答应访问 200 次,假设有个用户在第 59 秒的最后几毫秒瞬间发送 200 个央求,当 59 秒结束后 Counter 清零了,他鄙人一秒的时分又发送 200 个央求。

那么在 1 秒钟内这个用户发送了 2 倍的央求,这个是符合我们的规划逻辑的,这也是计数器方法的规划缺点,系统或许会接受歹意用户的很多央求,甚至击穿系统。这种方法尽管简略,但也有个大问题就是没有很好的处理单位时间的距离。

新来个技能总监,把限流完结的那叫一个高雅,敬服!

不过说实话,这个计数引用了锁,在高并发场景,这个方法或许不太实用,我主张将锁去掉,然后将 l.count++ 的逻辑通过原子计数处理,这样就可以保证 l.count 自增时不会被多个线程一起实行,即通过原子计数的方法完结限流。

为了不影响阅读,代码详见:github.com/lml20070115…

滑动窗口

滑动窗口是针对计数器存在的临界点缺点,所谓滑动窗口(Sliding window)是一种流量控制技能,这个词出现在 TCP 协议中。滑动窗口把固定时间片进行区分,而且随着时间的消逝,进行移动,固定数量的可以移动的格子,进行计数并判别阀值。

新来个技能总监,把限流完结的那叫一个高雅,敬服!

上图中我们用赤色的虚线代表一个时间窗口(一分钟),每个时间窗口有 6 个格子,每个格子是 10 秒钟。每过 10 秒钟时间窗口向右移动一格,可以看赤色箭头的方向。我们为每个格子都设置一个独立的计数器 Counter,假设一个央求在 0:45 访问了那么我们将第五个格子的计数器 +1(也是就是 0:40~0:50),在判别限流的时分需求把一切格子的计数加起来和设定的频次进行比较即可。

那么滑动窗口怎样处理我们上面遇到的问题呢?来看下面的图:

新来个技能总监,把限流完结的那叫一个高雅,敬服!

当用户在 0:59 秒钟发送了 200 个央求就会被第六个格子的计数器记载 +200,当下一秒的时分时间窗口向右移动了一个,此刻计数器已经记载了该用户发送的 200 个央求,所以再发送的话就会触发限流,则拒绝新的央求。

其实计数器就是滑动窗口啊,只不过只有一个格子罢了,所以想让限流做的更精确只需求区分更多的格子就可以了,为了更精确我们也不知道究竟该设置多少个格子,格子的数量影响着滑动窗口算法的精度,仍然有时间片的概念,无法底子处理临界点问题。

为了不影响阅读,代码详见:github.com/RussellLuo/…

漏桶

漏桶算法(Leaky Bucket),原理就是一个固定容量的漏桶,按照固定速率流出水滴。

用过水龙头都知道,翻开龙头开关水就会流下滴到水桶里,而漏桶指的是水桶下面有个漏洞可以出水,假设水龙头开的特别大那么水流速就会过大,这样就或许导致水桶的水满了然后溢出。

新来个技能总监,把限流完结的那叫一个高雅,敬服!

图片假设看不清,可单击图片并扩大。

一个固定容量的桶,有水流进来,也有水流出去。关于流进来的水来说,我们无法估计一共有多少水会流进来,也无法估计水流的速度。可是关于流出去的水来说,这个桶可以固定水流出的速率(处理速度),从而到达流量整形和流量控制的作用。

漏桶算法有以下特色:

  • 漏桶具有固定容量,出水速率是固定常量(流出央求)
  • 假设桶是空的,则不需流出水滴
  • 可以以任意速率流入水滴到漏桶(流入央求)
  • 假设流入水滴超出了桶的容量,则流入的水滴溢出(新央求被拒绝)

漏桶束缚的是常量流出速率(即流出速率是一个固定常量值),所以最大的速率就是出水的速率,不能出现突发流量。

为了不影响阅读,代码详见:github.com/lml20070115…

令牌桶

令牌桶算法(Token Bucket)是网络流量整形(Traffic Shaping)和速率束缚(Rate Limiting)中最常运用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并答应突发数据的发送。

新来个技能总监,把限流完结的那叫一个高雅,敬服!

图片假设看不清,可单击图片并扩大。

我们有一个固定的桶,桶里寄存着令牌(token)。一开始桶是空的,系统按固定的时间(rate)往桶里添加令牌,直到桶里的令牌数满,剩下的央求会被丢掉。当央求来的时分,从桶里移除一个令牌,假设桶是空的则拒绝央求或者阻塞。

令牌桶有以下特色:

  • 令牌按固定的速率被放入令牌桶中
  • 桶中最多寄存 B 个令牌,当桶满时,新添加的令牌被丢掉或拒绝
  • 假设桶中的令牌缺乏 N 个,则不会删去令牌,且央求将被限流(丢掉或阻塞等候)

令牌桶束缚的是均匀流入速率(答应突发央求,只需有令牌就可以处理,支撑一次拿3个令牌,4个令牌…),并答应必定程度突发流量,所以也是非常常用的限流算法。

为了不影响阅读,代码详见:github.com/lml20070115…

Redis + Lua 分布式限流

单机版限流仅能保护本身节点,但无法保护使用依托的各种服务,而且在进行节点扩容、缩容时也无法精确控制整个服务的央求束缚。

而分布式限流,以集群为维度,可以方便的控制这个集群的央求束缚,从而保护下流依托的各种服务资源。

分布式限流最要害的是要将限流服务做成原子化,我们可以凭仗 Redis 的计数器,Lua 实行的原子性,进行分布式限流,大致的 Lua 脚本代码如下:

localkey="rate.limit:"..KEYS[1]--限流KEY
locallimit=tonumber(ARGV[1])--限流巨细
localcurrent=tonumber(redis.call('get',key)or"0")
ifcurrent+1>limitthen--假设超出限流巨细
return0
else--央求数+1,并设置1秒过期
redis.call("INCRBY",key,"1")
redis.call("expire",key,"1")
returncurrent+1
end

限流逻辑(Java 言语):

publicstaticbooleanaccquire()throwsIOException,URISyntaxException{
Jedisjedis=newJedis("127.0.0.1");
FileluaFile=newFile(RedisLimitRateWithLUA.class.getResource("/").toURI().getPath()+"limit.lua");
StringluaScript=FileUtils.readFileToString(luaFile);
Stringkey="ip:"+System.currentTimeMillis()/1000;//其时秒
Stringlimit="5";//最大束缚
List<String>keys=newArrayList<String>();
keys.add(key);
List<String>args=newArrayList<String>();
args.add(limit);
Longresult=(Long)(jedis.eval(luaScript,keys,args));//实行lua脚本,传入参数
returnresult==1;
}

聊聊其它

上面的限流方法,主要是针对服务器进行限流,我们也可以对容器进行限流,比如 Tomcat、Nginx 等限流方法。

Tomcat 可以设置最大线程数(maxThreads),当并发超越最大线程数会排队等候实行;而 Nginx 供应了两种限流方法:一是控制速率,二是控制并发连接数。

关于 Java 言语,我们其实有相关的限流组件,比如我们常用的 RateLimiter,其实就是根据令牌桶算法,我们知道为什么唯一选用令牌桶么?

关于 Go 言语,也有该言语特定的限流方法,比如可以通过 channel 完结并发控制限流,也支撑第三方库 httpserver 完结限流,详见这篇 《Go 限流的常见方法》。

在实践的限流场景中,我们也可以控制单个 IP、城市、途径、设备 id、用户 id 等在必定时间内发送的央求数;假设是敞开渠道,需求为每个 appkey 设置独立的访问速率规矩。

限流对比

下面我们就对常用的线程战略,总结它们的优缺点,便于以后选型。

计数器:

  • 利益:固定时间段计数,完结简略,适用不太精准的场景;
  • 缺点:对距离没有很好处理,导致限流不能精准控制。

滑动窗口:

  • 利益:将固定时间段分块,时间比“计数器”凌乱,适用于稍微精准的场景;
  • 缺点:完结稍微凌乱,还是不能彻底处理“计数器”存在的距离问题。

漏桶:

  • 利益:可以很好的控制消费频率;
  • 缺点:完结稍微凌乱,单位时间内,不能多消费,感觉不太灵敏。

令牌桶:

  • 利益:可以处理“漏桶”不能灵敏消费的问题,又能避免过渡消费,强烈举荐;
  • 缺点:完结稍微凌乱,其它缺点没有想到。

Redis + Lua 分布式限流:

  • 利益:支撑分布式限流,有效保护下流依托的服务资源;
  • 缺点:依托 Redis,对距离没有很好处理,导致限流不能精准控制。

硬核举荐:
  • 去字节面试,直接让人出门左拐:Bean 生命周期都不知道!
  • 源码深度解析,Spring 怎样处理循环依托?
  • 总监又来了,人狠话不多,这篇 gRPC,小弟敬服!
  • 百度一面:谈谈 @Transactional 的原理和坑
  • 怎样才干到达阿里 P7 水平 ?
  • 新来个技能总监,把 RabbitMQ 讲的那叫一个透彻,敬服!

新来个技能总监,把限流完结的那叫一个高雅,敬服!