为什么要限流?
每个系统都有服务的上线,所以当流量超过服务极限能力时,系统可能会出现卡死、崩溃的情况,所以就有了降级和限流。限流其实就是:当高并发或者瞬时高并发时,为了保证系统的稳定性、可用性,系统以牺牲部分请求为代价或者延迟处理请求为代价,保证系统整体服务可用。
限流主要限制请求流量,保证当前服务、依赖服务不会被大流量彻底压死
限流方式
- 限制总并发数(比如数据库连接池、线程池)
- 限制瞬时并发数(如nginx的limit_conn模块,用来限制瞬时并发连接数)
- 限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limit_req模块,限制每秒的平均速率)
- 限制远程接口调用速率
- 限制MQ的消费速率。
- 可以根据网络连接数、网络流量、CPU或内存负载等来限流
限流算法有哪些?
常用的限流算法由:楼桶算法和令牌桶算法。本文不具体的详细说明两种算法的原理,原理会在接下来的文章中做说明。
1、漏桶算法
漏桶(Leaky Bucket)算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大会直接溢出(访问频率超过接口响应速率),然后就拒绝请求,可以看出漏桶算法能强行限制数据的传输速率.示意图如下:
可见这里有两个变量,一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。
因为漏桶的漏出速率是固定的参数,所以,即使网络中不存在资源冲突(没有发生拥塞),漏桶算法也不能使流突发(burst)到端口速率.因此,漏桶算法对于存在突发特性的流量来说缺乏效率.
漏桶算法比较好实现,在单机系统中可以使用队列来实现,在分布式环境中消息中间件或者Redis都是可选的方案。
2、令牌桶算法
令牌桶算法(Token Bucket)和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定1/QPS时间间隔(如果QPS=100,则间隔是10ms)往桶里加入Token(想象和漏洞漏水相反,有个水龙头在不断的加水),如果桶已经满了就不再加了.新请求来临时,会各自拿走一个Token,如果没有Token可拿了就阻塞或者拒绝服务.
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量.
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法来完成限流。
package org.java.base.bucket;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
import com.google.common.util.concurrent.RateLimiter;
public class GuavaRateLimiter {
final RateLimiter limit = RateLimiter.create(100);//10ms就有一个令牌
public void handlerWithLimiter(){
boolean hasToken = limit.tryAcquire(5, TimeUnit.MILLISECONDS);//尝试获得令牌,允许最大等待时间5ms内获得令牌
if(hasToken){
System.out.println(Thread.currentThread().getName()+" has token to do something...");
}else{
System.out.println(Thread.currentThread().getName()+" has not get the Token!!!!");
}
}
public static void main(String[] args) throws Exception {
GuavaRateLimiter gr = new GuavaRateLimiter();
Instant start = Instant.now();
Thread t1 = new Thread(()->gr.handlerWithLimiter(), "A01");
t1.start();
Thread t2 = new Thread(()->gr.handlerWithLimiter(), "A02");
t2.start();
Thread t3 = new Thread(()->gr.handlerWithLimiter(), "A03");
t3.start();
Thread t4 = new Thread(()->gr.handlerWithLimiter(), "A04");
t4.start();
Thread t5 = new Thread(()->gr.handlerWithLimiter(), "A05");
t5.start();
Thread t6 = new Thread(()->gr.handlerWithLimiter(), "A06");
t6.start();
Thread t7 = new Thread(()->gr.handlerWithLimiter(), "A07");
t7.start();
Thread t8 = new Thread(()->gr.handlerWithLimiter(), "A08");
t8.start();
t1.join();
t2.join();
t3.join();
t4.join();
t5.join();
t6.join();
t7.join();
t8.join();
System.out.println("It costs "+Duration.between(start, Instant.now()));
}
}
分布式限流
单节点模式下,使用RateLimiter进行限流一点问题都没有。但是…线上是分布式系统,布署了多个节点,而且多个节点最终调用的是同一个接口。虽然我们对单个节点能做到将QPS限制在400/s,但是多节点条件下,如果每个节点均是400/s,那么总请求就是
节点数x400/s,于是限流效果失效。
使用redis进行限流,其很好地解决了分布式环境下多实例所导致的并发问题。因为使用redis设置的计时器和计数器均是全局唯一的,不管多少个节点,它们使用的都是同样的计时器和计数器,因此可以做到非常精准的流控。同时,这种方案编码并不复杂
使用
Redis实现,存储两个key,一个用于计时,一个用于计数。请求每调用一次,计数器增加1,若在计时器时间内计数器未超过阈值,则可以处理任务
if(!cacheDao.hasKey(API_WEB_TIME_KEY)) {
cacheDao.putToValue(API_WEB_TIME_KEY,0,(long)1, TimeUnit.SECONDS);
}
if(cacheDao.hasKey(API_WEB_TIME_KEY)&&cacheDao.incrBy(API_WEB_COUNTER_KEY,(long)1) > (long)400) {
LOGGER.info("调用频率过快");
}
总结:
令牌桶可以在运行时控制和调整数据处理的速率,处理某时的突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。整体而言,令牌桶算法更优,但是实现更为复杂一些。
限流需要评估好,不可乱用,否则会正常流量出现一些奇怪的问题而导致用户抱怨。在实际应用时也不要太纠结
算法问题,因为一些限流算法实现是一样的只是描述不一样;具体使用哪种限流技术还是要根据实际场景来选择,不要一味去找最佳模式,白猫黑猫能解决问题的就是好猫。
参考:
- http://xiaobaoqiu.github.io/blog/2015/07/02/ratelimiter/
- http://redisdoc.com/string/incr.html
- http://www.cnblogs.com/zhengyun_ustc/archive/2012/11/17/topic1.html