限流的目的是经过对并发访问/请求进行限速或者一个时间窗口内的的请求进行限速来保护系统,一旦并发访问/请求达到限制速率或者超过其承受范围时候则能够拒绝服务、排队或引流。html
目前经常使用的限流算法有两个:漏桶算法和令牌桶算法。java
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。
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
对于不少应用场景来讲,除了要求可以限制数据的平均传输速率外,还要求容许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
https://en.wikipedia.org/wiki/Token_bucket
令牌桶算法最初来源于计算机网络。在网络传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送。令牌桶算法就实现了这个功能,可控制发送到网络上数据的数目,并容许突发数据的发送。
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型状况下,令牌桶算法用来控制发送到网络上的数据的数目,并容许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。若是令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中能够保存的最大令牌数永远不会超过桶的大小。
传送到令牌桶的数据包须要消耗令牌。不一样大小的数据包,消耗的令牌数量不同。
算法描述:
漏桶的出水速度是恒定的,那么意味着若是瞬时大流量的话,将有大部分请求被丢弃掉(也就是所谓的溢出)。
生成令牌的速度是恒定的,而请求去拿令牌是没有速度限制的。这意味,面对瞬时大流量,该算法能够在短期内请求拿到大量令牌,并且拿令牌的过程并非消耗很大的事情。
Guava RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
//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
突发的请求速率以下,
// 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秒桶中才能有令牌,且接下来的请求也整形为固定速率了。
平滑预热限流方式建立以下,
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======