:在开发高并发系统时,有三把利器用来保护系统:缓存、降级和限流。
限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等。你要开发一个限流的框架,那么必不可少的就是要选择一种合适的限流算法。限流算法很多,常见的有几类分别是:计数器算法
、滑动窗口算法
、漏桶算法
、令牌桶算法
,具体视业务场景,统计的精准度,限流维度而定。下面逐一讲解。
计数器算法是限流算法里最简单也是最容易实现的一种算法。主要用来限制一定时间内的总并发数,计数器限流只要一定时间内的总请求数超过设定的阀值则进行限流,是一种简单粗暴的总数量限流,而不是平均速率限流。比如数据库连接池、线程池、秒杀的并发数;
优点
:简单粗暴,单机在Java中可用Atomic等原子类或Semaphore,分布式就用Redis incr。
eg:我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,那么说明请求数过多,限流;如果该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter
缺点
:有一个十分致命的问题,那就是临界问题
。
eg:假设第1分钟中的第59秒的时候,来了100个请求,这个时候是没有超过100的限制。然后第2分钟的第1秒来了100个请求,相隔几毫秒,一下子进来200个请求,明显大于我们的限流阈值100。也就是说在时间窗口的重置节点处突发请求,可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。所以这个算法不够完美是不是?
为了解决这个临界区问题,我们引入了滑动窗口算法
。
滑动窗口,又称rolling window。上边提到的计数器算法中,比如限制1分钟内的访问次数,那么这个1分钟就是一个固定的时间窗口,而滑动窗口是将固定窗口再进行细分成多个窗口。
eg:比如将1分钟的固定窗口细分成6个窗口,那么每个窗口的时间就是10秒。如图整个红色的矩形框表示一个时间窗口,窗口是一直滑动的,每过10秒,我们的时间窗口就会往右滑动一格。按照上边我们的假设:假设第1分钟中的第59秒的时候,来了100个请求,落到灰色格子里,而第2分钟的1:00到达的100个请求会落在橘黄色的格子中,而这时我们的时间窗刚好检测到整个1分钟内(红色框),总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触发了限流。
Spring Cloud 中的熔断框架 Hystrix,以及 Spring Cloud Alibaba 中的Sentinel 都采用滑动窗口来做数据统计。
优点
:减少了临界值带来的并发超过阈值的问题。
由此可见,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
漏桶算法相对前面的计数算法更加柔性,它的原理也很简单,它是一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。漏桶限制的是请求的流出速率。漏桶中装的是请求。
优点
:是能够以固定的速率去控制流量,稳定性比较好。
可以认为就是我们常用的漏斗注水漏水的过程。漏桶接受以任意速率流入的水,并且以固定速率将水流出。当水超过桶的容量时,会被溢出,也就相当于被丢弃。
漏桶算法原理与消息队列思想有些类似,都是进行削峰填谷,经过漏桶后请求就能匀速平滑的流出。它是一种恒定速率的限流算法,不管请求量是多少,服务端的处理效率是恒定的。基于 MQ 来实现的生产者消费者模型,其实算是一种漏桶限流算法。
缺点
:就是无法应对突发流量的来袭,以及处理请求会有延迟
eg:假设你的漏桶出口固定了每秒钟只能通过100个请求,如果此时有150个请求,无论你后方的系统能不能抗住这150个请求,通过漏桶算法都会将另外50个请求进行拦截,只能等前面的100个请求结束后才能继续放行剩下的50个请求,无法应对突发流量的来袭。
面对突发请求,服务的处理速度和平常一样,这其实不是我们想要的。我们更想要的是面对突发流量时,在系统平稳运行的同时又能更快的处理请求,而不是一直按照统一的速率来处理。漏桶算法,可能更多的用来保护别人的接口。另外,漏桶中由于请求是暂存在桶中的,所以请求什么时候能被处理,则是有延时的,这并不符合互联网业务低延时的要求。
令牌桶是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌,填满了就丢弃令牌,请求是否被处理要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求。在流量低峰的时候,令牌桶会出现堆积,因此当出现瞬时高峰的时候,有足够多的令牌可以获取,令牌桶允许一定程度突发流量,只要有令牌就可以处理,支持一次拿多个令牌。令牌桶中装的是令牌。
网关层面的限流、或者接口调用的限流,都可以使用令牌桶算法,像 Google 的Guava,和 Redisson 的限流,都用到了令牌桶算法。
优点
:令牌桶算法是对漏斗算法的一种改进,除了能够起到限流的作用外,还允许一定程度的流量突发。
与漏桶算法相比,有可能导致短时间内的请求数上升(因为拿到令牌后,就可以访问接口,存在一瞬间将所有令牌拿走的情况),但不会有计数算法那样高的峰值(因为令牌数量是匀速增加的)。所以在应对突发流量的时候令牌桶表现的更佳。
一般自己调用自己的接口,接口会有一定的伸缩性,令牌桶算法,主要用来保护自己的服务器接口。
缺点
:例如令牌桶,假如系统上线时没有预热,那么可能会出现由于此时桶中还没有令牌,而导致请求被误杀的情况;
上面我们已经了解了每个限流算法的的特点,并且类比了一些应用场景。综上可知,虽然漏桶、令牌桶对比时间窗口类算法对流量的整形效果更好,但是它们也有各自的缺点。
令牌桶、漏桶算法更适合阻塞式限流的场景,即后台任务类的限流。
而基于时间窗口的限流则更适合互联网实施业务限流的场景,即能处理快速处理,不能处理及时响应调用方,避免请求出现过长的等待时间。
一般而言我们不需要自己实现限流算法来达到限流的目的,不管是接入层限流还是细粒度的接口限流其实都有现成的轮子使用,其实现也是用了上述我们所说的限流算法。
比如Google Guava 提供的限流工具类 RateLimiter,是基于令牌桶实现的,并且扩展了算法,支持预热功能。
阿里开源的限流框架Sentinel 中的匀速排队限流策略,就采用了漏桶算法。
Nginx 中的限流模块 limit_req_zone,采用了漏桶算法,还有 OpenResty 中的 resty.limit.req库等等。