Guava RateLimiter限流原理解析

来源:https://zhuanlan.zhihu.com/p/60979444

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

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

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

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

限流的基础算法

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

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

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

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

令牌桶和漏桶对比:

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
  • 令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿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库的主要维护者,二者配合"食用"更佳。

// 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
//当前存储令牌数
double storedPermits;
//最大存储令牌数
double maxPermits;
//添加令牌时间间隔
double stableIntervalMicros;
/*** 下一次请求可以获取令牌的起始时间* 由于RateLimiter允许预消费,上次请求预消费令牌后* 下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌*/
private long nextFreeTicketMicros = 0L;

平滑突发限流

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

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

public double acquire(int permits) {// 计算获取令牌所需等待的时间long microsToWait = reserve(permits);// 进行线程sleepstopwatch.sleepMicrosUninterruptibly(microsToWait);return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {checkPermits(permits);// 由于涉及并发操作,所以使用synchronized进行并发操作synchronized (mutex()) {return reserveAndGetWaitLength(permits, stopwatch.readMicros());}
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {// 计算从当前时间开始,能够获取到目标数量令牌时的时间long momentAvailable = reserveEarliestAvailable(permits, nowMicros);// 两个时间相减,获得需要等待的时间return max(momentAvailable - nowMicros, 0);
}

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

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

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {// 刷新令牌数,相当于每次acquire时在根据时间进行令牌的刷新resync(nowMicros);long returnValue = nextFreeTicketMicros;// 获取当前已有的令牌数和需要获取的目标令牌数进行比较,计算出可以目前即可得到的令牌数。double storedPermitsToSpend = min(requiredPermits, this.storedPermits);// freshPermits是需要预先支付的令牌,也就是目标令牌数减去目前即可得到的令牌数double freshPermits = requiredPermits - storedPermitsToSpend;// 因为会突然涌入大量请求,而现有令牌数又不够用,因此会预先支付一定的令牌数// waitMicros即是产生预先支付令牌的数量时间,则将下次要添加令牌的时间应该计算时间加上watiMicroslong waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)+ (long) (freshPermits * stableIntervalMicros);// storedPermitsToWaitTime在SmoothWarmingUp和SmoothBuresty的实现不同,用于实现预热缓冲期// SmoothBuresty的storedPermitsToWaitTime直接返回0,所以watiMicros就是预先支付的令牌所需等待的时间try {// 更新nextFreeTicketMicros,本次预先支付的令牌所需等待的时间让下一次请求来实际等待。this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);} catch (ArithmeticException e) {this.nextFreeTicketMicros = Long.MAX_VALUE;}// 更新令牌数,最低数量为0this.storedPermits -= storedPermitsToSpend;// 返回旧的nextFreeTicketMicros数值,无需为预支付的令牌多加等待时间。return returnValue;
}
// SmoothBurest
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {return 0L;
}

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

void resync(long nowMicros) {// 当前时间晚于nextFreeTicketMicros,所以刷新令牌和nextFreeTicketMicrosif (nowMicros > nextFreeTicketMicros) {// coolDownIntervalMicros函数获取每机秒生成一个令牌,SmoothWarmingUp和SmoothBuresty的实现不同// SmoothBuresty的coolDownIntervalMicros直接返回stableIntervalMicros// 当前时间减去要更新令牌的时间获取时间间隔,再除以添加令牌时间间隔获取这段时间内要添加的令牌数storedPermits = min(maxPermits,storedPermits+ (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());nextFreeTicketMicros = nowMicros;}// 如果当前时间早于nextFreeTicketMicros,则获取令牌的线程要一直等待到nextFreeTicketMicros,该线程获取令牌所需// 额外等待的时间由下一次获取的线程来代替等待。
}
double coolDownIntervalMicros() {return stableIntervalMicros;
}

下面我们举个例子,让大家更好的理解 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的相关代码如下所示,相关的逻辑都写在注释中。

// 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中间件,请大家继续关注。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/497404.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

脑洞大开:未来二十年将兴起的88个微型产业

来源:资本实验室概要:目前,我们正在进入一个指数级技术变革的时代。这个时代既催生出前所未有的科技巨头,其实也催生了一个微型创新的新时代。目前,我们正在进入一个指数级技术变革的时代。这个时代既催生出前所未有的…

并查集算法c语言版,并查集及其C程序实现.doc

并查集及其C程序实现等价关系与等价类从数学上看,等价类是一个对象(或成员)的集合,在此集合中的所有对象应满足等价关系。若用符号"≡"表示集合上的等价关系,那么对于该集合中的任意对象x,y, z,下列性质成立&#xff1a…

Web 服务策略断言语言 (WS-PolicyAssertions)

原文:http://www.microsoft.com/china/msdn/library/webservices/webservices/WebServicesPolicyAssertionsLanguage(WS-PolicyAssertions).mspxWeb 服务策略断言语言 (WS-PolicyAssertions) 发布日期: 4/26/2004| 更新日期: 4/26/2004版本 1…

周志华:严肃的研究者就不该去触碰强人工智能

来源:知识分子概要:关于人工智能,长期存在两种不同的目标或者理念。关于人工智能,长期存在两种不同的目标或者理念。一种是希望借鉴人类的智能行为,研制出更好的工具以减轻人类智力劳动,一般称为“弱人工智…

几种限流器(RateLimiter)原理与实现

来源:https://blog.csdn.net/netyeaxi/article/details/104270337 限流器(RateLimiter)主要有两种算法: 漏桶算法 令牌桶算法 它们都是网络世界中流量整形(Traffic Shaping)或速率限制(Rate Limiting)时经…

c语言如何随机选择入口,c语言随机排列-----适用于初学者

本程序只是实现了基本的数字随机排列,如有不懂,留言提问。。。// 随机排序.cpp : 定义控制台应用程序的入口点。//#include "stdafx.h"#include "time.h"#include "stdio.h"#include "stdlib.h"int _tmain(int …

The Best and the Worst

Joe Sanders has the most beautiful garden in our town. Nearly everybody enters for “The Nicest Garden Competition” each year,but Joe wins every time.Bill Friths garden is larger than Joes.Bill works harder than Joe and grows more flowlers and vegetables,…

guava限流器RateLimiter原理及源码分析

来源:https://www.cnblogs.com/zhandouBlog/p/11743660.html 前言 RateLimiter是基于令牌桶算法实现的一个多线程限流器,它可以将请求均匀的进行处理,当然他并不是一个分布式限流器,只是对单机进行限流。它可以应用在定时拉取接…

185页深度报告 扒一扒AI金融的老底【附下载】

来源:智东西概要:2016年,中国爆出8家独角兽,总估值964亿美元位冠全球;2017年,毕马威全球百佳金融科技企业前三甲,蚂蚁金服、众安保险、趣店,皆来自中国;过往两年&#xf…

夹娃娃动画Android,手机模拟抓娃娃

手机模拟抓娃娃让你通过手机足不出户也能感受娃娃机的乐趣,萌趣的卡通形象,清新治愈的设计风格,简单上手的玩法,赶快加入进来冲击最高分吧,点击下载手机模拟抓娃娃开始你的挑战!手机模拟抓娃娃介绍手机模拟抓娃娃游戏是一款模拟进行的真人在线抓娃娃的掌上控制的休闲…

Inline Method(内联函数)

一个函数的本体与名称同样清楚易懂 int getRating() {return moreThanFiveLateDeliveries() ? 2 : 1; }boolean moreThanFiveLateDeliveries() {return numberOfLateDelivers > 5; } 重构后 int getRating() {return (numberOfLateDelivers > 5) ? 2 : 1; }

XML 简单操作

<?xml:namespace prefix o /> <?xml version"1.0" encoding"gb2312"?><bookstore> <book genre"fantasy" ISBN"2-3631-4"> <title>Oberons Legacy</title> <author>Corets, Ev…

android友盟错误日志,Taro(React-native)集成友盟错误日志分析U-App移动统计

1、先去友盟官网注册应用&#xff0c;获取到appkey&#xff0c;友盟移动统计分析U-App&#xff0c;这个步骤就不贴出来了&#xff0c;需要注意的是ios和Android 不能使用同一个appkey&#xff0c;需要分别创建两个应用&#xff0c;应用名称可以在后面加上平台名称&#xff0c;例…

2018年中国新零售市场研究报告——概念、模式与案例【附下载】

来源&#xff1a;亿欧概要&#xff1a;“新零售” 之“新”在于顺势下的“变化”&#xff0c;不应该局限于“阿里巴巴的新零售”。报告尝试从一个更宽广的视角&#xff0c;对当前零售业变化的背景和各种驱动因素进行分析&#xff0c;总结当下时间段零售行业参与者的新动作&…

[导入]ASP.NET 配置节架构

ASP.NET 配置节架构文章来源:http://blog.csdn.net/21aspnet/archive/2004/11/04/167417.aspx转载于:https://www.cnblogs.com/zhaoxiaoyang2/archive/2004/11/05/816261.html

重构--思维导图

#原图 System.out.println("https://www.processon.com/view/60fa8c441e085366ea4c2b9e?fromnew1");

Android代码导出数据库,导入/导出到android sqlite数据库

我看过几篇关于如何在android中导入和导出数据库的帖子,我找到了这些代码,但我似乎无法使它工作.我收到错误java.io.filenotfoundexception / storage / sdcard0 / BackupFolder / DatabaseName&#xff1a;打开失败的ENOENT(没有这样的文件或目录).我改变了一些东西,但我仍然没…

谷歌Jeff Dean团队提出利用深度学习对「电子健康记录」数据进行分析,可提高医疗诊断预测的准确性

原文来源&#xff1a;arXiv作者&#xff1a;Alvin Rajkomar、Eyal Oren、Kai Chen、Andrew M. Dai、Nissan Hajaj、Peter J. Liu、Xiaobing Liu, Mimi Sun、Patrik Sundberg、Hector Yee、Kun Zhang、Yi Zhang、Gavin E. Duggan、Gerardo Flores、Michaela Hardt、Jamie Irvine…

想太多……

想太多总是伤神&#xff0c;不晓得是因为早上打针打太多了还是晚上想太多了&#xff0c;现在有点头疼…… 被成全的人&#xff0c;应该是幸福的人&#xff0c;还有什么好抱怨的呢……亲眼看到成全我的人的痛苦&#xff0c;难道不应该更珍惜现在的快乐和幸福么&#xff1f; 不去…

Inline Temp(内联临时变量)

一个临时变量&#xff0c;只被一个简单表达式赋值一次&#xff0c;而它妨碍了其他重构手法 double basePrice anOrder.basePrice(); return basePrice > 1000; 重构后 return anOrder.basePrice() > 1000;