其实这是一个政治任务
最近公司要求所有系统都接入限流,天天给研发发邮件,不接就直接抄送领导,并且逐级上升,最高到总监,就问你怕不怕!估计总监都不认识我这号人。
也不知道是中间件的KPI还是公司的KPI,反正小兵都是被干。
什么是限流
最早接触限流应该是几年前,那时候公司用了spring cloud全家桶。有一个同事分享了hystrix,介绍了限流与熔断。
当时没有业务场景没有用到,也就没有深究,那时候公司连监控也没有,也不管什么qps,rt,只管业务向前跑。
高并发系统保护有三大手段:缓存,限流,降级。
缓存有很多:本地缓存;第三方缓存:redis,memcached;分布式缓存等。下次专门研究一下,这里主要看下限流。
当系统面对突然增加的流量或者超过系统承受能力的请求时,可以通过限制请求数量,让其等待,排队,降级甚至拒绝服务,从而达到保护系统的一种手段。
在JAVA中,我理解线程池的后拒绝策略其实就是一种限流手段,传送门:java线程池
或者在缓存中,有时候为了防止缓存雪崩,也会用上信号量Semaphore做限流
限流算法
成熟的限流中间件有上面提到的Hystrix,还有阿里开源的Sentinel,以及一些大公司自研的中间件产品。但是前后的算法原理出入并不大。
通用的限流算法
- QPS限流算法:通过限制单位时间段内调用量来限流
- 漏桶算法:Leaky Bucket算法来限流
- 令牌桶算法:Token Bucket算法来限流
QPS算法
QPS:queries per second,每秒钟处理完的请求数量,传送门:系统吞吐量基本概念
控制单位时间内放过的请求数来限流,如下
- 假设限流规则是qps为3
- 图上表示前后2秒钟,前一秒,最后时刻有两个请求过来,满足限流条件,放过
- 下一秒开始,又有三个请求过来,在限流之内,放过
- 第四个请求,达到上限,被限流
上面就是QPS的限流过程,它计算简单,并且是否限流只跟请求数有关,比较符合我们的预期与直觉。而且可以通过拉长限流周期来应对突发流量。如1秒限流10个,想要放过瞬间20个请求,可以把限流配置改成3秒限流30个。拉长限流周期会有一定风险,这个需要仔细评估。
但是,细心的可以发现,在上图的临界值处理上,这种算法,是有问题:前一秒,与下一秒的计算节点,其实是通过了2倍的QPS,如果把标红的地方视为一秒,其实是通过了五个请求,是不符合限流规则的。
而且,对于突发请求的处理也不是很友好,放过的请求不均匀,突发流量时,请求总在限流周期的前一部分放过。如10秒限100个,高流量时放过的请求总是在限流周期的第一秒。
Guava的令牌桶算法
google的guava库提供了很多有用的工具包,比如集合类处理,缓存,布隆过滤器等,当然也少不了令牌桶算法的限流工具RateLimiter
下面是借用网上的经典算法图
- 系统会按照恒定的速率往桶里放令牌Token,比如1/QPS时间间隔:假设QPS是100,则时间间隔是10ms
- 如果令牌桶放满了,则不再放Token了
- 每一个请求过来,会拿走一个令牌Token
- 如果没有Token可拿了就阻塞或者拒绝服务
令牌桶算法的好处是
-
放过的流量比较均匀,有利于保护系统。因为令牌是均匀产生的,所以放过的流量也肯定是均匀的,对于突增流量,不会存在QPS算法的只会放过前面限流周期最前面流量的问题
-
没有时间边界,在任意时间点逻辑是统一的。这样可以有效避免QPS算法的边界值问题
-
令牌桶的另外一个好处是可以方便的改变速度. 一旦需要提高速率,则按需提高放入桶中的令牌的速率. 一般会定时(比如100毫秒)往桶中增加一定数量的令牌, 有些变种算法则实时的计算应该增加的令牌的数量
漏桶算法
未完待续...
RateLimiter简介
Hello world来热身,这个是RateLimiter源码中这一个例子
@Test
public void test1() {
ExecutorService executorService = Executors.newFixedThreadPool(2);
RateLimiter rateLimiter = RateLimiter.create(2.0);
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 50; i++) {
rateLimiter.acquire();
executorService.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程" + Thread.currentThread().getName());
});
}
long endTime = System.currentTimeMillis();
System.out.println("一共耗时:" + (endTime - beginTime) / 1000);
输出
一个50个任务,每秒只允许提交2个,理论上应该是25秒左右完成
RateLimiter主要接口
RateLimiter其实是一个abstract类,但是它提供了几个static方法用于创建RateLimiter:
常用的第一个方法:创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
提供了两种获取令牌的方法,不带参数表示获取一个令牌.如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0
tryAcquire是尝试获取令牌,分为待超时时间和不带超时时间两种:
acquire()与tryAcquire()不同点,就是acquire()是阻塞方法,而tryAcquire是非阻塞的