计数器-漏桶-滑动窗口-令牌桶-掌握4种常用限流算法 (计数器维修视频教程)
限流是一种限制最大流量的技术,以防止操作频率超过定义的限制。当系统能提供的最大并发有限,而请求又太多时,就需要限流。例如,在秒杀或大促活动中,瞬时大量请求涌入,服务器可能无法处理,这时就需要限流来保护系统。
速率限制通过限制在给定时间段内可以到达 API 的请求数量来保护服务免受意外或恶意过度使用。如果没有速率限制,任何用户都可以用请求轰炸服务器,导致其他用户饿死的情况。
速率限制算法
速率限制策略可应用于以下参数: 每秒请求数 (QPS) 每分钟请求数 每小时请求数 每个用户每秒请求数 每个用户每分钟请求数 下面介绍四种常见的限流算法:- 漏桶算法
- 令牌桶算法
- 固定时间窗口算法
- 滑动时间窗口算法
漏桶算法
漏桶算法是一种简单直观的算法,它的概念就像一个固定容量的漏桶,桶内按照常量固定速率流出水滴。当桶是空的时,不需要流出水滴。可以以任意速率流入水滴到漏桶中。 如果流入水滴超出了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。这种算法的优点是它可以平滑请求的突发并以恒定的速率处理它们。它也很容易在负载均衡器上实现,并且对每个用户来说都是高效的内存。优点:
- 保持到服务器的恒定接近均匀的流量,无论请求的数量如何
缺点:
- 请求的爆发可能会填满存储桶,导致新请求的匮乏
- 不能保证请求在给定的时间内完成
令牌桶算法
令牌桶算法假设限制为 2r/s,则按照 500 毫秒的固定速率往桶中添加令牌。桶中最多存放 b 个令牌,当桶满时,新添加的令牌被丢弃或拒绝。 当一个 n 个字节大小的数据包到达时,将从桶中删除 n 个令牌,接着数据包被发送到网络上。如果桶中的令牌不足 n 个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。优点:
- 在短时间内可以请求拿到大量令牌
- 拿令牌的过程并不是消耗很大量
缺点:
- 可能导致分布式环境中的竞争条件
固定时间窗口算法
在固定的时间窗口内,可以允许固定数量的请求进入。超过数量就拒绝或者排队,等待下一个时间段进入。这种实现计数器限流方式由于是在一个时间间隔内进行限制,可能导致系统在时间临界点左右遭到攻击。缺点:
- 存在时间临界点缺陷
滑动时间窗口算法
滑动窗口算法是把固定时间片进行划分,并且随着时间移动,通过这种方式可以巧妙地避开计数器的临界点问题。滑动窗口算法可以有效地规避计数器算法中时间临界点的问题,但是仍然存在时间片段的概念。 选择合适的限流算法需要根据系统的具体情况和需求来确定。你们都是怎么确保系统不被突然的访问流量压垮的?
确保系统的高可用,要做的事情非常多,比如使用Redis缓存数据库的数据,降低数据库的压力,同时也要注意缓存穿透、雪崩、击穿等问题;但要是说到“不要被突增的访问量击垮”,通常就会到我们常说的分布式架构三板斧:限流、熔断、降级。
01.限流
限流理解起来很简单,比如故宫每天只卖八万张票,超过八万的游客,无法买票进入,因为如果超过八万人,景点的工作人员可能就忙不过来,过于拥挤的景点也会影响游客的体验和心情,并且还会有安全隐患;只卖N张票,这就是一种限流的手段。
软件架构中的限流也一样,就是流量徒增的时候,只允许一部分流量进来,而多余的那部分,就拒绝掉。
通常我们可以通过限流算法达到这样的效果,比如计数器法、滑动窗口法、漏桶算法、令牌桶算法,每个算法的详解之前的文章有介绍过,这里就不在占用篇幅了。上面的例子中,故宫每天只卖八万张票,有点儿类似于令牌桶算法,票就相当于令牌,只有拿到令牌的请求,才能访问到服务。
另外限流可以针对不同的系统或业务流程限流,比如核心系统A要做限流,B系统调用A系统很重要,C系统调用A系统相对来说不是那么重要,所以当A系统有些扛不住的时候,可以限制C系统的调用次数,保证B系统的稳定运行。
02.熔断
现实生活中,保险丝的作用就是熔断,可以在发生短路的时候自动跳闸,保护家电。
在我们大部分应用场景中,A系统调B系统接口,B系统再调C系统接口这样的场景非常多,这就是调用链路:A->B->C->D;每个系统的承载上限肯定是不一样的,比如流量徒增,D系统达到承载上限了,D系统的接口响应非常慢,这样可能会导致A/B/C调用它时出现超时等待的情况;如果进一步恶化,会导致链路雪崩,从一个服务的故障,变成了多个系统的故障。
这时候熔断就排上用场了,如果短时间内有大量的请求超时,那么就意味着这个系统出现了故障,那么就没有必要再去访问这个服务了,这时候就要使用熔断,断开这条链路。
熔断器还可以自动诊断下游服务的状态,如果服务恢复的话,那么再慢慢释放请求,直到故障发生前的状态。
03.降级
服务降级既可以通过代码自动判断,比如上文的服务限流中说到,当流量徒增,可以限制不重要的系统或服务的访问量,这里的谁重要谁不重要,就算是服务级别的区分,当访问量徒增,哪些系统是可以自动降级的。
服务降级也可以人工根据突发情况切换;比如在某些服务节点的时候(例如双11,618),为了保证购物和支付的正常运行,会禁用一些不重要的服务;甚至在购物和支付两者之间,购物更重要,所以可以提前预付或者延迟支付。
总之,限流、熔点和降级都是在流量徒增、过大时,保证系统稳定的手段。
我将持续分享Java开发、架构设计、程序员职业发展等方面的见解,希望能得到你的关注。
分布式解决方案之:限流
限流在日常生活中限流很常见,例如去有些景区玩,每天售卖的门票数是有限的,例如 2000 张,即每天最多只有 2000 个人能进去游玩。那在我们工程上限流是什么呢?限制的是 「流」,在不同场景下「流」的定义不同,可以是 每秒请求数、每秒事务处理数、网络流量 等等。通常意义我们说的限流指代的是限制到达系统的并发请求数,使得系统能够正常的处理部分用户的请求,来保证系统的稳定性。
日常的业务上有类似秒杀活动、双十一大促或者突发新闻等场景,用户的流量突增,后端服务的处理能力是有限的,如果不能处理好突发流量,后端服务很容易就被打垮。另外像爬虫之类的不正常流量,我们对外暴露的服务都要以最大恶意为前提去防备调用者。我们不清楚调用者会如何调用我们的服务,假设某个调用者开几十个线程一天二十四小时疯狂调用你的服务,如果不做啥处理咱服务基本也玩完了,更胜者还有ddos攻击。
对于很多第三方开放平台来说,不仅仅要防备不正常流量,还要保证资源的公平利用,一些接口资源不可能一直都被一个客户端占着,也需要保证其他客户端能正常调用。
计数器限流也就是最简单的限流算法就是计数限流了。例如系统能同时处理 100 个请求,保存一个计数器,处理了一个请求,计数器就加一,一个请求处理完毕之后计数器减一。每次请求来的时候看看计数器的值,如果超过阈值就拒绝。计数器的值要是存内存中就算单机限流算法,如果放在第三方存储里(例如Redis中)集群机器访问就算分布式限流算法。
一般的限流都是为了限制在指定时间间隔内的访问量,因此还有个算法叫固定窗口。
它相比于计数限流主要是多了个时间窗口的概念,计数器每过一个时间窗口就重置。规则如下:
这种方式也会面临一些问题,例如固定窗口临界问题:假设系统每秒允许 100 个请求,假设第一个时间窗口是 0-1s,在第 0.55s 处一下次涌入 100 个请求,过了 1 秒的时间窗口后计数清零,此时在 1.05 s 的时候又一下次涌入100个请求。虽然窗口内的计数没超过阈值,但是全局来看在 0.55s-1.05s 这 0.1 秒内涌入了 200 个请求,这其实对于阈值是 100/s 的系统来说是无法接受的。
为了解决这个问题,业界又提出另外一种限流算法,即滑动窗口限流。
滑动窗口限流解决固定窗口临界值的问题,可以保证在任意时间窗口内都不会超过阈值。相对于固定窗口,滑动窗口除了需要引入计数器之外还需要记录时间窗口内每个请求到达的时间点,因此对内存的占用会比较多。
规则如下,假设时间窗口为 1 秒:
但是滑动窗口和固定窗口都无法解决短时间之内集中流量的冲击问题。 我们所想的限流场景是:每秒限制 100 个请求。希望请求每 10ms 来一个,这样我们的流量处理就很平滑,但是真实场景很难控制请求的频率,因为可能就算我们设置了1s内只能有100个请求,也可能存在 5ms 内就打满了阈值的情况。当然对于这种情况还是有变型处理的,例如设置多条限流规则。不仅限制每秒 100 个请求,再设置每 10ms 不超过 2 个,不过带来的就是比较差的用户体验。
而漏桶算法,可以解决时间窗口类的痛点,使得流量更加平滑。
如下图所示,水滴持续滴入漏桶中,底部定速流出。如果水滴滴入的速率大于流出的速率,当存水超过桶的大小的时候就会溢出。
规则如下:
水滴对应的就是请求。
与线程池实现的方式方式如出一辙。
面对突发请求,服务的处理速度和平时是一样的,这并非我们实际想要的。我们希望的是在突发流量时,在保证系统平稳的同时,也要尽可能提升用户体验,也就是能更快地处理并响应请求,而不是和正常流量一样循规蹈矩地处理。
而令牌桶在应对突击流量的时候,可以更加的“激进”。
令牌桶其实和漏桶的原理类似,只不过漏桶是定速地流出,而令牌桶是定速地往桶里塞入令牌,然后请求只有拿到了令牌才能通过,之后再被服务器处理。
当然令牌桶的大小也是有限制的,假设桶里的令牌满了之后,定速生成的令牌会丢弃。
规则:
令牌桶的原理与JUC的Semaphore 信号量很相似,信号量可控制某个资源被同时访问的个数,其实和拿令牌思想一样,不同的是一个是拿信号量,一个是拿令牌。信号量用完了返还,而令牌用了不归还,因为令牌会定时再填充。
对比漏桶算法可以看出 令牌桶更适合应对突发流量 ,假如桶内有 100 个令牌,那么这100个令牌可以马上被取走,而不像漏桶那样匀速的消费。不过上面批量获取令牌也会致使一些新的问题出现,比如导致一定范围内的限流误差,举个例子你取了 10 个此时不用,等下一秒再用,那同一时刻集群机器总处理量可能会超过阈值,所以现实中使用时,可能不会去考虑redis频繁读取问题,转而直接采用一次获取一个令牌的方式,具体采用哪种策略还是要根据真实场景而定。
1、计数器 VS 固定窗口 VS 滑动窗口
2、漏桶算法 VS 令牌桶算法
总的来说
单机限流和分布式限流本质上的区别在于 “阈值” 存放的位置,单机限流就是“阀值”存放在单机部署的服务/内存中,但我们的服务往往是集群部署的,因此需要多台机器协同提供限流功能。像上述的计数器或者时间窗口的算法,可以将计数器存放至 Redis 等分布式 K-V 存储中。又如滑动窗口的每个请求的时间记录可以利用 Redis 的zset存储,利用 ZREMRANGEBYSCORE删除时间窗口之外的数据,再用ZCARD 计数,
可以看到,每个限流都有个阈值,这个阈值如何定是个难点。定大了服务器可能顶不住,定小了就“误杀”了,没有资源利用最大化,对用户体验不好。一般的做法是限流上线之后先预估个大概的阈值,然后不执行真正的限流操作,而是采取日志记录方式,对日志进行分析查看限流的效果,然后调整阈值,推算出集群总的处理能力,和每台机子的处理能力(方便扩缩容)。然后将线上的流量进行重放,测试真正的限流效果,最终阈值确定,然后上线。
其实真实的业务场景很复杂,需要限流的条件和资源很多,每个资源限流要求还不一样。
一般而言,我们不需要自己实现限流算法来达到限流的目的,不管是接入层限流还是细粒度的接口限流,都有现成的轮子使用,其实现也是用了上述我们所说的限流算法。
具体的使用还是很简单的,有兴趣的同学可以自行搜索,对内部实现感兴趣的同学可以下个源码看看,学习下生产级别的限流是如何实现的。
免责声明:本文转载或采集自网络,版权归原作者所有。本网站刊发此文旨在传递更多信息,并不代表本网赞同其观点和对其真实性负责。如涉及版权、内容等问题,请联系本网,我们将在第一时间删除。同时,本网站不对所刊发内容的准确性、真实性、完整性、及时性、原创性等进行保证,请读者仅作参考,并请自行核实相关内容。对于因使用或依赖本文内容所产生的任何直接或间接损失,本网站不承担任何责任。