基于RateLimiter的服务接口限流实例

前景回顾:
《基于计数器的服务接口限流实例》

一、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解析(一) ——设计哲学与快速使用

你可能感兴趣的:(基于RateLimiter的服务接口限流实例)