限流 - Guava RateLimiter

限流

限流的目的是经过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦并发访问/请求达到限制速率或者超过其承受范围时候则能够拒绝服务、排队或引流。html

目前经常使用的限流算法有两个:漏桶算法和令牌桶算法。java

漏桶算法-Leaky Bucket

http://en.wikipedia.org/wiki/Leaky_bucket算法

根据wiki上的介绍,Leaky Bucket实际上有两种不一样的含义。api

1)as a meter(做为计量工具)缓存

2)as a queue(做为调度队列)网络

Leaky Bucket 做为计量工具,以下所示:并发

如图,桶自己具备一个恒定的速率往下漏水,而上方时快时慢地会有水进入桶中。当桶还未满时,上方的水能够加入。一旦水满,上方的水就没法加入了。桶满正是算法中的一个的关键触发条件(即流量异常判断成立的条件)。而此条件下如何处理上方漏下的水,则有了下面两种常见的方式。app

在桶满水以后,常见的两种处理方式为:ssh

1)暂时拦截住上方水的向下流动,等待桶中的一部分水漏走后,再放行上方水。工具

2)溢出的上方水直接抛弃。

将水看做网络通讯中数据包的抽象,则方式1起到的效果称为Traffic Shaping,方式2起到的效果称为Traffic Policing。

Comparing Traffic Policing and Traffic Shaping for Bandwidth Limiting

The following diagram illustrates the key difference. Traffic policing propagates bursts. When the traffic rate reaches the configured maximum rate, excess traffic is dropped (or remarked). The result is an output rate that appears as a saw-tooth with crests and troughs. In contrast to policing, traffic shaping retains excess packets in a queue and then schedules the excess for later transmission over increments of time. The result of traffic shaping is a smoothed packet output rate.

参考:https://www.cisco.com/c/en/us/support/docs/quality-of-service-qos/qos-policing/19645-policevsshape.html

令牌桶算法-Token Bucket

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

https://en.wikipedia.org/wiki/Token_bucket

令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并容许突发数据的发送。

令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型状况下,令牌桶算法用来控制发送到网络上的数据的数目,并容许突发数据的发送。

大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。若是令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中能够保存的最大令牌数永远不会超过桶的大小。

传送到令牌桶的数据包须要消耗令牌。不一样大小的数据包,消耗的令牌数量不同。

令牌桶这种控制机制基于令牌桶中是否存在令牌来指示何时能够发送流量。令牌桶中的每个令牌都表明一个字节。若是令牌桶中存在令牌,则容许发送流量;而若是令牌桶中不存在令牌,则不容许发送流量。所以,若是令牌桶中有足够的令牌,那么流量就能够以峰值速率发送。

算法描述:

  1. 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中(每秒会有r个令牌放入桶中);
  2. 假设桶中最多能够存放b个令牌。若是令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
  3. 当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌(不一样大小的数据包,消耗的令牌数量不同),而且数据包被发送到网络;
  4. 若是令牌桶中少于n个令牌,那么不会删除令牌,而且认为这个数据包在流量限制以外(n个字节,须要n个令牌。该数据包将被缓存或丢弃);
  5. 算法容许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包能够以不一样的方式处理:(1)它们能够被丢弃;(2)它们能够排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;(3)它们能够继续发送,但须要作特殊标记,网络过载的时候将这些特殊标记的包丢弃。

令牌桶算法VS漏桶算法

漏桶

漏桶的出水速度是恒定的,那么意味着若是瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。

令牌桶

生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法能够在短期内请求拿到大量令牌,并且拿令牌的过程并非消耗很大的事情。

Java 实现 - Guava ReateLimtier

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

SmoothBursty

//RateLimiter.create(5) 表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

输入以下,

0.0
0.191546
0.19138
0.197585
0.194843
0.195557
  1. RateLimiter.create(5) 表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;
  2. limiter.acquire()表示消费一个令牌,若是当前桶中有足够令牌则成功(返回值为0),若是桶中没有令牌则暂停一段时间,好比发令牌间隔是200毫秒,则等待200毫秒后再去消费令牌(如上测试用例返回的为0.198239,差很少等待了200毫秒桶中才有令牌可用),这种实现将突发请求速率平均为了固定请求速率。

突发的请求速率以下,

// guava Stopwatch
Stopwatch stopwatch = Stopwatch.createUnstarted();

//RateLimiter.create(5) 表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌;
RateLimiter limiter = RateLimiter.create(5);
System.out.println(limiter.acquire(10)); // 0.0,获取令牌成功

System.out.println("获取令牌start...");
stopwatch.start();
System.out.println(limiter.acquire(1));
long time = stopwatch.elapsed(TimeUnit.MILLISECONDS);
System.out.println("获取令牌花费的时间=" + time);

System.out.println(limiter.acquire(1));

输出以下,

获取令牌start...
1.996403
获取令牌花费的时间=2002
0.193232

第一秒突发了10个请求,令牌桶算法也容许了这种突发(容许消费将来的令牌),但接下来的limiter.acquire(1)将等待差很少2秒桶中才能有令牌,且接下来的请求也整形为固定速率了。

**SmoothWarmingUp **

平滑预热限流方式建立以下,

RateLimiter limiter = RateLimiter.create(5, 1000, TimeUnit.MILLISECONDS);

System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

Thread.sleep(1000L);
System.out.println("sleep end...");

System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());
System.out.println(limiter.acquire());

输出以下,

0.0
0.51702
0.355476
0.220346
0.197056
sleep end...
0.0
0.365465
0.221425
0.199815
0.199381

能够看到刚开始获取令牌所花费的时候比较大,随着时间的推移,所花费的时间愈来愈少,趋于稳定,说明速率趋于稳定。这就是所谓的预热。

======END======

相关文章
相关标签/搜索