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

 点击上方“方志朋”,选择“置顶或者星标”java

你的关注意义重大!nginx

 

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

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

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

 好比说,咱们须要限制方法被调用的并发数不能超过100(同一时间并发数),则咱们能够用信号量 Semaphore实现。可若是咱们要限制方法在一段时间内平均被调用次数不超过100,则须要使用 RateLimiter数据库

限流的基础算法

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

 

 

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

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

 

 

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

 

 

令牌桶和漏桶对比:

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

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

  • 令牌桶容许必定程度的突发,而漏桶主要目的是平滑流出速率;

Guava RateLimiter

  Guava是Java领域优秀的开源项目,它包含了Google在Java项目中使用一些核心库,包含集合(Collections),缓存(Caching),并发编程库(Concurrency),经常使用注解(Common annotations),String操做,I/O操做方面的众多很是实用的函数。  Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。

 

 

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

  1. // RateLimiter提供了两个工厂方法,最终会调用下面两个函数,生成RateLimiter的两个子类。

  2. static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {

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

  4. rateLimiter.setRate(permitsPerSecond);

  5. return rateLimiter;

  6. }

  7. static RateLimiter create(

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

  9. double coldFactor) {

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

  11. rateLimiter.setRate(permitsPerSecond);

  12. return rateLimiter;

  13. }

平滑突发限流

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

  1. public void testSmoothBursty() {

  2. RateLimiter r = RateLimiter.create(5);

  3. while (true) {

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

  5. }

  6. /**

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

  8. * get 1 tokens: 0.0s

  9. * get 1 tokens: 0.182014s

  10. * get 1 tokens: 0.188464s

  11. * get 1 tokens: 0.198072s

  12. * get 1 tokens: 0.196048s

  13. * get 1 tokens: 0.197538s

  14. * get 1 tokens: 0.196049s

  15. */

  16. }

  RateLimiter使用令牌桶算法,会进行令牌的累积,若是获取令牌的频率比较低,则不会致使等待,直接获取令牌。

  1. public void testSmoothBursty2() {

  2. RateLimiter r = RateLimiter.create(2);

  3. while (true)

  4. {

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

  6. try {

  7. Thread.sleep(2000);

  8. } catch (Exception e) {}

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

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

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

  12. System.out.println("end");

  13. /**

  14. * output:

  15. * get 1 tokens: 0.0s

  16. * get 1 tokens: 0.0s

  17. * get 1 tokens: 0.0s

  18. * get 1 tokens: 0.0s

  19. * end

  20. * get 1 tokens: 0.499796s

  21. * get 1 tokens: 0.0s

  22. * get 1 tokens: 0.0s

  23. * get 1 tokens: 0.0s

  24. */

  25. }

  26. }

  RateLimiter因为会累积令牌,因此能够应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,可是因为此时令牌桶中有累积的令牌,足以快速响应。   RateLimiter在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。

  1. public void testSmoothBursty3() {

  2. RateLimiter r = RateLimiter.create(5);

  3. while (true)

  4. {

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

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

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

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

  9. System.out.println("end");

  10. /**

  11. * output:

  12. * get 5 tokens: 0.0s

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

  14. * get 1 tokens: 0.194007s

  15. * get 1 tokens: 0.196267s

  16. * end

  17. * get 5 tokens: 0.195756s

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

  19. * get 1 tokens: 0.194603s

  20. * get 1 tokens: 0.196866s

  21. */

  22. }

  23. }

平滑预热限流

  RateLimiter的 SmoothWarmingUp是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提高到配置的速率。  好比下面代码中的例子,建立一个平均分发令牌速率为2,预热期为3分钟。因为设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是造成一个平滑线性降低的坡度,频率愈来愈高,在3秒钟以内达到本来设置的频率,之后就以固定的频率输出。这种功能适合系统刚启动须要一点时间来“热身”的场景。

  1. public void testSmoothwarmingUp() {

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

  3. while (true)

  4. {

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

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

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

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

  9. System.out.println("end");

  10. /**

  11. * output:

  12. * get 1 tokens: 0.0s

  13. * get 1 tokens: 1.329289s

  14. * get 1 tokens: 0.994375s

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

  16. * end

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

  18. * get 1 tokens: 0.497828s

  19. * get 1 tokens: 0.49449s

  20. * get 1 tokens: 0.497522s

  21. */

  22. }

  23. }

源码分析

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

  1. //SmoothRateLimiter.java

  2. //当前存储令牌数

  3. double storedPermits;

  4. //最大存储令牌数

  5. double maxPermits;

  6. //添加令牌时间间隔

  7. double stableIntervalMicros;

  8. /**

  9. * 下一次请求能够获取令牌的起始时间

  10. * 因为RateLimiter容许预消费,上次请求预消费令牌后

  11. * 下次请求须要等待相应的时间到nextFreeTicketMicros时刻才能够获取令牌

  12. */

  13. private long nextFreeTicketMicros = 0L;

平滑突发限流

  RateLimiter的原理就是每次调用 acquire时用当前时间和 nextFreeTicketMicros进行比较,根据两者的间隔和添加单位令牌的时间间隔 stableIntervalMicros来刷新存储令牌数 storedPermits。而后acquire会进行休眠,直到 nextFreeTicketMicros

  acquire函数以下所示,它会调用 reserve函数计算获取目标令牌数所需等待的时间,而后使用 SleepStopwatch进行休眠,最后返回等待时间。

  1. public double acquire(int permits) {

  2. // 计算获取令牌所需等待的时间

  3. long microsToWait = reserve(permits);

  4. // 进行线程sleep

  5. stopwatch.sleepMicrosUninterruptibly(microsToWait);

  6. return 1.0 * microsToWait / SECONDS.toMicros(1L);

  7. }

  8. final long reserve(int permits) {

  9. checkPermits(permits);

  10. // 因为涉及并发操做,因此使用synchronized进行并发操做

  11. synchronized (mutex()) {

  12. return reserveAndGetWaitLength(permits, stopwatch.readMicros());

  13. }

  14. }

  15. final long reserveAndGetWaitLength(int permits, long nowMicros) {

  16. // 计算从当前时间开始,可以获取到目标数量令牌时的时间

  17. long momentAvailable = reserveEarliestAvailable(permits, nowMicros);

  18. // 两个时间相减,得到须要等待的时间

  19. return max(momentAvailable - nowMicros, 0);

  20. }

  reserveEarliestAvailable是刷新令牌数和下次获取令牌时间 nextFreeTicketMicros的关键函数。它有三个步骤,一是调用 resync函数增长令牌数,二是计算预支付令牌所需额外等待的时间,三是更新下次获取令牌时间 nextFreeTicketMicros和存储令牌数 storedPermits

 这里涉及 RateLimiter的一个特性,也就是能够预先支付令牌,而且所需等待的时间在下次获取令牌时再实际执行。详细的代码逻辑的解释请看注释。

  1. final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {

  2. // 刷新令牌数,至关于每次acquire时在根据时间进行令牌的刷新

  3. resync(nowMicros);

  4. long returnValue = nextFreeTicketMicros;

  5. // 获取当前已有的令牌数和须要获取的目标令牌数进行比较,计算出能够目前便可获得的令牌数。

  6. double storedPermitsToSpend = min(requiredPermits, this.storedPermits);

  7. // freshPermits是须要预先支付的令牌,也就是目标令牌数减去目前便可获得的令牌数

  8. double freshPermits = requiredPermits - storedPermitsToSpend;

  9. // 由于会忽然涌入大量请求,而现有令牌数又不够用,所以会预先支付必定的令牌数

  10. // waitMicros便是产生预先支付令牌的数量时间,则将下次要添加令牌的时间应该计算时间加上watiMicros

  11. long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)

  12. + (long) (freshPermits * stableIntervalMicros);

  13. // storedPermitsToWaitTime在SmoothWarmingUp和SmoothBuresty的实现不一样,用于实现预热缓冲期

  14. // SmoothBuresty的storedPermitsToWaitTime直接返回0,因此watiMicros就是预先支付的令牌所需等待的时间

  15. try {

  16. // 更新nextFreeTicketMicros,本次预先支付的令牌所需等待的时间让下一次请求来实际等待。

  17. this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);

  18. } catch (ArithmeticException e) {

  19. this.nextFreeTicketMicros = Long.MAX_VALUE;

  20. }

  21. // 更新令牌数,最低数量为0

  22. this.storedPermits -= storedPermitsToSpend;

  23. // 返回旧的nextFreeTicketMicros数值,无需为预支付的令牌多加等待时间。

  24. return returnValue;

  25. }

  26. // SmoothBurest

  27. long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

  28. return 0L;

  29. }

  resync函数用于增长存储令牌,核心逻辑就是 (nowMicros-nextFreeTicketMicros)/stableIntervalMicros。当前时间大于 nextFreeTicketMicros时进行刷新,不然直接返回。

  1. void resync(long nowMicros) {

  2. // 当前时间晚于nextFreeTicketMicros,因此刷新令牌和nextFreeTicketMicros

  3. if (nowMicros > nextFreeTicketMicros) {

  4. // coolDownIntervalMicros函数获取每机秒生成一个令牌,SmoothWarmingUp和SmoothBuresty的实现不一样

  5. // SmoothBuresty的coolDownIntervalMicros直接返回stableIntervalMicros

  6. // 当前时间减去要更新令牌的时间获取时间间隔,再除以添加令牌时间间隔获取这段时间内要添加的令牌数

  7. storedPermits = min(maxPermits,

  8. storedPermits

  9. + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());

  10. nextFreeTicketMicros = nowMicros;

  11. }

  12. // 若是当前时间早于nextFreeTicketMicros,则获取令牌的线程要一直等待到nextFreeTicketMicros,该线程获取令牌所需

  13. // 额外等待的时间由下一次获取的线程来代替等待。

  14. }

  15. double coolDownIntervalMicros() {

  16. return stableIntervalMicros;

  17. }

 下面咱们举个例子,让你们更好的理解 resync和 reserveEarliestAvailable函数的逻辑。

 好比 RateLimiter的 stableIntervalMicros为500,也就是1秒发两个令牌,storedPermits为0,nextFreeTicketMicros为155391849 5748。线程一acquire(2),当前时间为155391849 6248,首先 resync函数计算,(1553918496248 - 1553918495748)/500 = 1,因此当前可获取令牌数为1,可是因为能够预支付,因此nextFreeTicketMicros= nextFreeTicketMicro + 1 * 500 = 155391849 6748。线程一无需等待。

 紧接着,线程二也来acquire(2),首先 resync函数发现当前时间早于 nextFreeTicketMicros,因此没法增长令牌数,因此须要预支付2个令牌,nextFreeTicketMicros= nextFreeTicketMicro + 2 * 500 = 155391849 7748。线程二须要等待155391849 6748时刻,也就是线程一获取时计算的nextFreeTicketMicros时刻。一样的,线程三获取令牌时也须要等待到线程二计算的nextFreeTicketMicros时刻。

平滑预热限流

 上述就是平滑突发限流RateLimiter的实现,下面咱们来看一下加上预热缓冲期的实现原理。   SmoothWarmingUp实现预热缓冲的关键在于其分发令牌的速率会随时间和令牌数而改变,速率会先慢后快。表现形式以下图所示,令牌刷新的时间间隔由长逐渐变短。等存储令牌数从maxPermits到达thresholdPermits时,发放令牌的时间价格也由coldInterval下降到了正常的stableInterval。

 

 

  SmoothWarmingUp的相关代码以下所示,相关的逻辑都写在注释中。

  1. // SmoothWarmingUp,等待时间就是计算上图中梯形或者正方形的面积。

  2. long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {

  3. /**

  4. * 当前permits超出阈值的部分

  5. */

  6. double availablePermitsAboveThreshold = storedPermits - thresholdPermits;

  7. long micros = 0;

  8. /**

  9. * 若是当前存储的令牌数超出thresholdPermits

  10. */

  11. if (availablePermitsAboveThreshold > 0.0) {

  12. /**

  13. * 在阈值右侧而且须要被消耗的令牌数量

  14. */

  15. double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);

  16. /**

  17. * 梯形的面积

  18. *

  19. * 高 * (顶 * 底) / 2

  20. *

  21. * 高是 permitsAboveThresholdToTake 也就是右侧须要消费的令牌数

  22. * 底 较长 permitsToTime(availablePermitsAboveThreshold)

  23. * 顶 较短 permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)

  24. */

  25. micros = (long) (permitsAboveThresholdToTake

  26. * (permitsToTime(availablePermitsAboveThreshold)

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

  28. /**

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

  30. */

  31. permitsToTake -= permitsAboveThresholdToTake;

  32. }

  33. /**

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

  35. */

  36. micros += (stableIntervalMicros * permitsToTake);

  37. return micros;

  38. }

  39. double coolDownIntervalMicros() {

  40. /**

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

  42. * 为 maxPermits

  43. */

  44. return warmupPeriodMicros / maxPermits;

  45. }

后记

  RateLimiter只能用于单机的限流,若是想要集群限流,则须要引入 redis或者阿里开源的 sentinel中间件,请你们继续关注。

参考

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

  • https://segmentfault.com/a/1190000012875897

-更多文章-

微服务架构·基础篇

数据库中间件详解 | 珍藏版

Java 性能优化的 45 个细节

哥们,你真觉得你会作这道JVM面试题?

如何阅读Java源码?

-关注我-

 

 

看完了,帮我点个“好看”鸭

点鸭点鸭

↓↓↓↓

相关文章
相关标签/搜索