• 军事专栏
  • 军史回眸
  • 中国军情
  • 快捷搜索:  国内新闻    as  很长时间  88888  dfdas  很长时间 0  很长时间 @#

    超详细的Guava RateLimiter限流原理解析

    限流是保护高并发系统的三把利器之一,另外两个是缓存和降级。限流在很多场景中用来限制并发和请求量,比如说秒杀抢购,保护自身系统和下游系统不被巨型流量冲垮等。

    限流的目的是通过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦达到限制速率则可以拒绝服务或进行流量整形。

    常用的限流方式和场景有:限制总并发数(比如数据库连接池、线程池)、限制瞬时并发数(如nginx的limitconn模块,用来限制瞬时并发连接数,Java的Semaphore也可以实现)、限制时间窗口内的平均速率(如Guava的RateLimiter、nginx的limitreq模块,限制每秒的平均速率);其他还有如限制远程接口调用速率、限制MQ的消费速率。另外还可以根据网络连接数、网络流量、CPU或内存负载等来限流。

    比如说,我们需要限制方法被调用的并发数不能超过100(同一时间并发数),则我们可以用信号量 Semaphore实现。可如果我们要限制方法在一段时间内平均被调用次数不超过100,则需要使用 RateLimiter。限流的基础算法

    我们先来讲解一下两个限流相关的基本算法:漏桶算法和令牌桶算法。

    超详细的Guava RateLimiter限流原理解析

    从上图中,我们可以看到,就像一个漏斗一样,进来的水量就好像访问流量一样,而出去的水量就像是我们的系统处理请求一样。当访问流量过大时,这个漏斗中就会积水,如果水太多了就会溢出。

    漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照固定频率从队列头取出请求进行处理。如果请求量大,则会导致队列满,那么新来的请求就会被抛弃。

    超详细的Guava RateLimiter限流原理解析

    令牌桶算法则是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不同,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。

    超详细的Guava RateLimiter限流原理解析

    令牌桶和漏桶对比:

    令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;

    令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;

    令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率;Guava RateLimiter

    Guava是Java领域优秀的开源项目,它包含了Google在Java项目中使用一些核心库,包含集合(Collections),缓存(Caching),并发编程库(Concurrency),常用注解(Common annotations),String操作,I/O操作方面的众多非常实用的函数。

    Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

    超详细的Guava RateLimiter限流原理解析

    RateLimiter的类图如上所示,其中 RateLimiter是入口类,它提供了两套工厂方法来创建出两个子类。这很符合《Effective Java》中的用静态工厂方法代替构造函数的建议,毕竟该书的作者也正是Guava库的主要维护者,二者配合"食用"更佳。// RateLimiter提供了两个工厂方法,最终会调用下面两个函数,生成RateLimiter的两个子类。

    static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {

    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);

    rateLimiter.setRate(permitsPerSecond);

    return rateLimiter;

    }

    static RateLimiter create(

    SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit,

    double coldFactor) {

    RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit, coldFactor);

    rateLimiter.setRate(permitsPerSecond);

    return rateLimiter;

    }

    平滑突发限流

    使用 RateLimiter的静态方法创建一个限流器,设置每秒放置的令牌数为5个。返回的RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。public void testSmoothBursty() {

    RateLimiter r = RateLimiter.create(5);

    while (true) {

    System.out.println("get 1 tokens: " + r.acquire() + "s");

    }

    /**

    * output: 基本上都是0.2s执行一次,符合一秒发放5个令牌的设定。

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.182014s

    * get 1 tokens: 0.188464s

    * get 1 tokens: 0.198072s

    * get 1 tokens: 0.196048s

    * get 1 tokens: 0.197538s

    * get 1 tokens: 0.196049s

    */

    }

    RateLimiter使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。public void testSmoothBursty2() {

    RateLimiter r = RateLimiter.create(2);

    while (true)

    {

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    try {

    Thread.sleep(2000);

    } catch (Exception e) {}

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("end");

    /**

    * output:

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.0s

    * end

    * get 1 tokens: 0.499796s

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.0s

    * get 1 tokens: 0.0s

    */

    }

    }

    RateLimiter由于会累积令牌,所以可以应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。

    RateLimiter在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。public void testSmoothBursty3() {

    RateLimiter r = RateLimiter.create(5);

    while (true)

    {

    System.out.println("get 5 tokens: " + r.acquire(5) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("end");

    /**

    * output:

    * get 5 tokens: 0.0s

    * get 1 tokens: 0.996766s 滞后效应,需要替前一个请求进行等待

    * get 1 tokens: 0.194007s

    * get 1 tokens: 0.196267s

    * end

    * get 5 tokens: 0.195756s

    * get 1 tokens: 0.995625s 滞后效应,需要替前一个请求进行等待

    * get 1 tokens: 0.194603s

    * get 1 tokens: 0.196866s

    */

    }

    }

    平滑预热限流

    RateLimiter的 SmoothWarmingUp是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。

    比如下面代码中的例子,创建一个平均分发令牌速率为2,预热期为3分钟。由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来“热身”的场景。public void testSmoothwarmingUp() {

    RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);

    while (true)

    {

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("get 1 tokens: " + r.acquire(1) + "s");

    System.out.println("end");

    /**

    * output:

    * get 1 tokens: 0.0s

    * get 1 tokens: 1.329289s

    * get 1 tokens: 0.994375s

    * get 1 tokens: 0.662888s 上边三次获取的时间相加正好为3秒

    * end

    * get 1 tokens: 0.49764s 正常速率0.5秒一个令牌

    * get 1 tokens: 0.497828s

    * get 1 tokens: 0.49449s

    * get 1 tokens: 0.497522s

    */

    }

    }

    源码分析

    看完了 RateLimiter的基本使用示例后,我们来学习一下它的实现原理。先了解一下几个比较重要的成员变量的含义。//SmoothRateLimiter.java

    超详细的Guava RateLimiter限流原理解析

    SmoothWarmingUp的相关代码如下所示,相关的逻辑都写在注释中。// SmoothWarmingUp,等待时间就是计算上图中梯形或者正方形的面积。

    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

    /**

    * 当前permits超出阈值的部分

    */

    double availablePermitsAboveThreshold = storedPermits - thresholdPermits;

    long micros = 0;

    /**

    * 如果当前存储的令牌数超出thresholdPermits

    */

    if (availablePermitsAboveThreshold > 0.0) {

    /**

    * 在阈值右侧并且需要被消耗的令牌数量

    */

    double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);

    /**

    * 梯形的面积

    *

    * 高 * (顶 * 底) / 2

    *

    * 高是 permitsAboveThresholdToTake 也就是右侧需要消费的令牌数

    * 底 较长 permitsToTime(availablePermitsAboveThreshold)

    * 顶 较短 permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)

    */

    micros = (long) (permitsAboveThresholdToTake

    * (permitsToTime(availablePermitsAboveThreshold)

    + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0);

    /**

    * 减去已经获取的在阈值右侧的令牌数

    */

    permitsToTake -= permitsAboveThresholdToTake;

    }

    /**

    * 平稳时期的面积,正好是长乘宽

    */

    micros += (stableIntervalMicros * permitsToTake);

    return micros;

    }

    double coolDownIntervalMicros() {

    /**

    * 每秒增加的令牌数为 warmup时间/maxPermits. 这样的话,在warmuptime时间内,就就增张的令牌数量

    * 为 maxPermits

    */

    return warmupPeriodMicros / maxPermits;

    }

    后记

    RateLimiter只能用于单机的限流,如果想要集群限流,则需要引入 redis或者阿里开源的 sentinel中间件,请大家继续关注。

    个人博客: 「链接」

    个人微信公众号: remcarpediem 可以直接向我询问问题。

    参考

    https://jinnianshilongnian.iteye.com/blog/2305117

    https://segmentfault.com/a/1190000012875897

    本文地址:http://www.zyxbb.com/sznew/202020.html 转载请注明出处!