前景回顾:
《基于计数器的服务接口限流实例》
一、RateLimit中acquire的使用
在前面这篇文章中,我们使用了计数器来做服务接口的限流,它最大的问题在于,无法将请求均匀地分摊到单位时间内的每个时间段上。行业术语就是无法平滑地分摊请求。
本文的主角Guava中的RateLimiter就可以很好地平滑地分摊请求。关于RateLimiter所涉及的漏桶及令牌桶算法原理,本文不再赘述,可以参考文末的参考文章。
com.google.guava
guava
28.0-jre
RateLimiter的使用非常简单,如下是最基本的使用实例:
@Slf4j
@RestController
public class UserMailRest {
/**
* 每秒投入2个令牌
* */
private RateLimiter rateLimiter = RateLimiter.create(2);
@GetMapping("/getUserMail")
public String getUserMail(){
rateLimiter.acquire();
log.info("请求得到了服务!");
return "OK";
}
}
如上代码的作用就是规定每秒产生2个令牌,控制一秒内最多只能有2个请求得到服务。按照RateLimiter的设计原则,这两个令牌的投放时间间隔为1/2=0.5秒,如此,能达到每隔0.5秒接受一个请求对其服务的目的。
我们使用Jmeter作为测试工具,设置10个用户线程,在一秒内各自发起一次请求。
得到的结果如下:
2019-12-23 21:32:44.714 : 请求得到了服务!
2019-12-23 21:32:44.809 : 请求得到了服务!
2019-12-23 21:32:44.910 : 请求得到了服务!
2019-12-23 21:32:45.217 : 请求得到了服务!
2019-12-23 21:32:45.714 : 请求得到了服务!
2019-12-23 21:32:46.264 : 请求得到了服务!
2019-12-23 21:32:46.714 : 请求得到了服务!
2019-12-23 21:32:47.214 : 请求得到了服务!
2019-12-23 21:32:47.714 : 请求得到了服务!
2019-12-23 21:32:48.214 : 请求得到了服务!
现在模拟下这10个请求发起后,服务端都发生了什么:
- 40秒,服务器启动,此时没有请求到来,服务器中积累了2个令牌;
- 40秒~44秒,此时还是没有请求到来,服务器每秒都会产生2个令牌,但是因为前面存储的令牌没有被消费,而当前最多只能存放2个令牌,所以在此期间产生的令牌全部被放弃;
- 44秒714的时候,第一个请求到来,此时服务器已经存放了两个历史令牌,所以请求不用等待,直接获取令牌得到服务;
- 44秒809,第二个请求到来,同样立马得到服务;
- 44秒910,第三个请求获取了44秒下半秒本来就应该产生的一个令牌,因此也能立马得到服务;
- 45秒217,第四个请求获取了45秒上半秒本来就应该产生的一个令牌...
以此类推,可以看到,我们原本在1秒内发起的10个请求,在5秒内,差不多每隔0.5秒被服务接受。从而使得请求的时间分布变得平滑,不会在某一个毫秒内集中被服务。
只不过需要注意的是,RateLimiter会存储空闲的令牌,但是最多只能存储一个时间单位的令牌数目,从而会使得空闲后突然激增的请求也能得到服务。
二、RateLimiter中tryAcquire的使用
acquire的使用是为了将所有的请求平滑地分布到后续的时间段内,但有的时候,请求实在太多了,我们需要对调用过于频繁的请求给予拒绝服务的响应,此时就需要用到tryAcquire了。
@Slf4j
@RestController
public class UserMailRest {
/**
* 每秒投入2个令牌
* */
private RateLimiter rateLimiter = RateLimiter.create(2);
@GetMapping("/getUserMail")
public String getUserMail(){
if(!rateLimiter.tryAcquire()){
log.warn("请求过于频繁,拒绝服务!");
return "请求过于频繁,拒绝服务!";
}
log.info("请求得到服务!");
return "OK";
}
}
如上只有在tryAcquire失败的情况下,拒绝服务,执行结果如下:
2019-12-23 21:57:32.083 : 请求得到服务!
2019-12-23 21:57:32.083 : 请求得到服务!
2019-12-23 21:57:32.126 : 请求得到服务!
2019-12-23 21:57:32.224 : 请求过于频繁,拒绝服务!
2019-12-23 21:57:32.324 : 请求过于频繁,拒绝服务!
2019-12-23 21:57:32.426 : 请求过于频繁,拒绝服务!
2019-12-23 21:57:32.526 : 请求过于频繁,拒绝服务!
2019-12-23 21:57:32.626 : 请求得到服务!
2019-12-23 21:57:32.726 : 请求过于频繁,拒绝服务!
2019-12-23 21:57:32.829 : 请求过于频繁,拒绝服务!
同样的,我们分析下服务端都发生了什么:
- 30秒,服务器启动,此时没有请求到来,服务器中积累了2个令牌;
- 30秒~32秒,此时还是没有请求到来,服务器每秒都会产生2个令牌,但是因为前面存储的令牌没有被消费,而当前最多只能存放2个令牌,所以在此期间产生的令牌全部被放弃;
- 32秒083的时候,第一个请求到来,此时服务器已经存放了两个历史令牌,所以请求不用等待,直接获取令牌得到服务;
- 32秒083,第二个请求到来,同样立马得到服务;
- 32秒126,第三个请求获取了32秒上半秒本来就应该产生的一个令牌,因此也能立马得到服务;
- 32秒224~32秒526,这些请求都没有令牌可以获取,因为此时,32秒下半秒的令牌还没有被释放,因此,全部都拒绝服务;
- 32秒626,第八个请求获得了32秒下半秒本来就应该产生的令牌,因此得到服务;
- 剩下的两个请求都是在32秒下半秒发生的,因此获取不到令牌,因此拒绝服务;
三、总结
好了,关于RateLimiter的常见使用方法主要就是以上讲解的acquire以及tryAcquire,通过实例后的分析我们大致也能了解其工作原理。
RateLimiter相较于计数器的限流方案来说,最大的特点就是限制请求在单位时间内平滑地对服务器进行访问,从而不会发生,在一个毫秒内爆发全部的请求,瞬间压垮服务的情况。
但是,和计数器一样,存在一个很致命的问题。它们都只是限制单位时间内请求的数量,但并不能限制服务器上并发的数量。比如,1秒内允许200个访问,但是这200个访问是很耗时的LongCall,从而导致第n秒的时候,服务器上可能就会有200*n个线程的并发,最终超过系统能承载的最大并发数,压垮系统。
四、参考文献:
RateLimiter解析(一) ——设计哲学与快速使用