限流,从令牌桶算法到RateLimiter源码

限流,是服务或者应用对自身保护的一种手段,通过限制或者拒绝调用方的流量,来保证自身的负载。

常用的限流算法有两种:漏桶算法和令牌桶算法

漏桶算法

    思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法

    原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。


下面的内容主要讨论令牌桶算法。

仔细讨论之前,先看下一下基于分布式缓存实现的令牌桶的流程图。


以下是对流程图的讲解:

1. key是否存在。因为流程图是基于分布式缓存做的集群限流,需要根据不同key做统计,第一次访问初始化key。

2. 如果key不存在,初始化令牌桶,防止初始令牌数量,并且设置key过期时间为interval*2。这里的初始令牌数量一般可以设置成限流阈值,比如限流10qps,初始值可以设置成10,来应对一开始的流量。interval是间隔时间,比如限流阈值10qps,interval设置为1s。过期时间是缓存中key的时间,interval*2是为了防止key过期无法拦截流量。

3. 如果key存在,将当前请求时间和当前key的最后放置令牌时间做比较。如果间隔超过interval,进入第4步,间隔未超过interval,进入第5步。

4. 间隔已经超过1s,直接放置令牌到最大数量。

5. 间隔没有超过1s,定义delta为时间差,放置令牌数=delta/(1/qps)。放入令牌时保证令牌数不超过桶的容量。同时,重置放入令牌的时间。

6. 从桶中获取令牌,获取令牌成功,执行请求;获取令牌时间,拒绝请求。

以上是对令牌桶算法的一种实现,接下来会具体分析guava RateLimiter的源码,RateLimiter的原理和上述实现类似,但是会有部分区别。

最基础的使用RateLimiter的姿势如下:

RateLimiter rateLimiter = RateLimiter.create(10.0);

rateLimiter.acquire();

create方法用于构建既定速度的实例,acquire方法使用阻塞方式获取令牌。

首先进入源码,看下create方法:

static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {

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

      rateLimiter.setRate(permitsPerSecond);

      return rateLimiter;

}

这里我们看到,会构建一个SmoothBursty实例,并且给这个实例设置速率。

/**

* This implements a "bursty" RateLimiter, where storedPermits are translated to

* zero throttling. The maximum number of permits that can be saved (when the RateLimiter is

* unused) is defined in terms of time, in this sense: if a RateLimiter is 2qps, and this

* time is specified as 10 seconds, we can save up to 2 * 10 = 20 permits.

*/

SmoothBursty的注释翻译如下:

这是一个“突发性”的RateLimiter实现,这里存储令牌数可以被转义成“零节流”。存储的最大令牌数被存储成(如果RateLimiter实例有一段时间没有被获取令牌)一个时间形式。举例:如果一个RateLimiter实例的速率是2qps,并且maxBurstSeconds时间是10,那么最多可以存储20个令牌(令牌桶容量)。

看一下SmoothBursty的构造函数:

SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {

      super(stopwatch);

      this.maxBurstSeconds = maxBurstSeconds;

}

下面简单介绍下SleepingStopwatch是什么。

@VisibleForTesting

abstract static class SleepingStopwatch {

  /*   * We always hold the mutex when calling this. TODO(cpovirk): Is that important? Perhaps we need

  * to guarantee that each call to reserveEarliestAvailable, etc. sees a value >= the previous?

  * Also, is it OK that we don't hold the mutex when sleeping?

  */

      abstract long readMicros();

      abstract void sleepMicrosUninterruptibly(long micros);

      static final SleepingStopwatch createFromSystemTimer() {

            return new SleepingStopwatch() {

                  final Stopwatch stopwatch = Stopwatch.createStarted();

                  @Override

                  long readMicros() {

                        return stopwatch.elapsed(MICROSECONDS);

                  }

                  @Override

                  void sleepMicrosUninterruptibly(long micros) {

                        if (micros > 0) {

                              Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS);

                        }

                  }

            };

      }

}


SleepingStopWatch是一个可sleep的秒表,起始时间是构建StopWatch的时间,sleepMicrosUninterruptibly方法支持不受中断的sleep,sleep是当前线程的sleep。

以上构建方法完成,下面再来看一下acquire的源码。

public double acquire(int permits) {

      long microsToWait = reserve(permits);

      stopwatch.sleepMicrosUninterruptibly(microsToWait);

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

}

第一行是获取令牌需要等待时间;第二行是线程sleep时间,如果令牌足够,这里会返回0,无需sleep;第三行是返回等待时间值,单位转换成秒。

接下来看下reserve实现。

final long reserve(int permits) {

      checkPermits(permits);

      synchronized (mutex()) {

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

      }

}

获取锁之后,直接调用reserveAndGetWaitLength方法,传入参数是需要获取的令牌数、秒表的当前时间。

final long reserveAndGetWaitLength(int permits, long nowMicros) {

      long momentAvailable = reserveEarliestAvailable(permits, nowMicros);

      return max(momentAvailable - nowMicros, 0);

}

首先计算获取令牌需要的时间节点,如果时间节点小于当前时间,无需等待;如果时间节点在当前时间节点之后,需要sleep线程,sleep时间是momentAvailable和nowMicros的差值。

下面看下reserveEarliestAvailable,计算时间节点的实现。

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {

      resync(nowMicros);

      long returnValue = nextFreeTicketMicros;

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

      double freshPermits = requiredPermits - storedPermitsToSpend;

      long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)       + (long) (freshPermits * stableIntervalMicros);


    try {

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

      } catch (ArithmeticException e) {

            this.nextFreeTicketMicros = Long.MAX_VALUE;

      }

      this.storedPermits -= storedPermitsToSpend;

      return returnValue;

}

这里面有几个变量需要注意下:

    nextFreeTicketMicros 下次允许获取令牌的时间,这个时间是因为RateLimiter允许透支而存在的,比如当前令牌桶只有一个令牌,一个请求来获取5个令牌,请求会成功,但是nextFreeTicketMicros往后推4个时间片段,在当前时间推移到nextFreeTicketMicros之前,所有请求都将等待。如果长时间没有请求到来,这个值会是过去的一个时间值。

    storedPermits 当前令牌桶剩余的令牌数。

    stableIntervalMicros 时间片段值,qps为5的话,时间片段是200ms。

    maxPermits 令牌桶的容量。

下面开始分析代码。

resync(nowMicros);//根据当前时间和nextFreeTicketMicros,往令牌桶放置令牌,最多不超过令牌桶的maxPermits。

long returnValue = nextFreeTicketMicros; //赋值语句

double storedPermitsToSpend =min(requiredPermits,this.storedPermits);//请求的令牌数和令牌桶当前令牌数做比较,取较小值,storedPermitsToSpend是指需要消耗当前令牌桶的令牌数量。

double freshPermits = requiredPermits - storedPermitsToSpend;//需要透支的令牌数量

long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)

    +(long) (freshPermits *stableIntervalMicros);//如果不透支,waitMicros为0,下次请求可以正常获取令牌;如果透支,需要将nextFreeTicketMicros往后推。

try {

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

} catch (ArithmeticException e) {

  this.nextFreeTicketMicros = Long.MAX_VALUE;

}

//将nextFreeTicketMicros往后推

this.storedPermits -= storedPermitsToSpend;//清算令牌桶的令牌。


至此,最基础的创建RateLimiter和阻塞获取令牌的过程已经分析完毕。

总结:

1. 文章前端的流程图和guava RateLimiter的实现类似。

2. guava RateLimiter支持透支,如果每次获取单个令牌,那么透支将不会生效。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,440评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,814评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,427评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,710评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,625评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,511评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,162评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,311评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,262评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,278评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,989评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,583评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,664评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,904评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,274评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,856评论 2 339

推荐阅读更多精彩内容