面试必备技能之服务限流

2,082 阅读6分钟

原文地址:my.oschina.net/u/732520/bl…
作者:乘风

前段时间很多人最想做的事可能是看刘德华演唱会。是的,刘德华计划于2018年的12月15日至2019年的1月3日在香港连唱20场。

他的演唱会火爆到什么程度呢?有个女粉丝花了600美元买了张只需580港币的门票,而且还是从美国飞到HK。为什么人家愿意多花好几倍的钱去买呢,是因为演唱会门票有限,而且这可能是刘德华最后一次演唱会,很多人也是为了圆儿时的梦不惜找各种渠道,各种加价,只为了一睹天王的风采。

主办方只卖固定数量的门票并不是为了让别人多花钱去买黄牛票,而是场馆只能容纳指定数量的人,为了限制场馆人流量,构建一个安全的环境。

同理,软件应用系统也是如此。一台服务器的处理能力有限,如果对请求不加限制最后造成对后果是响应缓慢,甚至造成服务宕机,所以了解限流并在应用系统中对请求限流很有必要。

一、什么是服务限流

什么是服务限流呢?限流即限制并发量,限制某一段时间只有指定数量的请求进入后台服务器,遇到流量高峰期或者流量突增时,把流量速率限制在系统所能接受的合理范围之内,不至于让系统被高流量击垮。限流与缓存、降级统称为分布式系统的三大利器,最终目的都是用来保护系统稳定运行。

二、如何实现服务限流

怎么实现服务限流呢?开发过程中或多或少会接触到服务限流,比如tomcat限制最大连接数、数据库连接池限制连接数、nginx限制ip访问数、秒杀、抢购等。这些都是通过限制一个时间窗口内的请求数,当达到设置的最大请求数后,会让后续请求进入等待队列或直接拒绝,防止系统过载。这些限流是怎么做到的呢,或者说限流主要有哪些方式呢?很多人把限流归纳成4种场景,其实也不外乎这4种场景,它们分别是:

1)限制总并发数或请求数

从系统层面维护一个计数器,每来一个请求就将计数器加1,请求处理完后将计数器减1,当计数器大于设定的阀值时,拒绝请求或将请求放入等待队列。这是最简单粗暴的限流方式,实现起来比较简单,但它有个很明显的缺陷是如果某一时间段系统受到恶意攻击(即突发请求),若并发数大于阀值时,后续有效的请求都会被拒绝,因而这种方法在现实中的使用并不常见,实现如下:

try {
     if(holder.incrementAndGet() > limit) {
        //refuse-business:拒绝请求
     } else {
        //do-business:业务处理
     }
} finally {
      holder.decrementAndGet();
}

2)限制接口的总并发或请求数

针对每个接口维护一个计数器,每来一个请求就将对应接口的计数器加1,请求处理完后将计数器减1,当计数器大于设定的阀值时,拒绝请求或将请求放入等待队列。这种方式较之第一种稍复杂些,如果要用aop去实现的话,则需要定义多个切面,维护成本也随之加大,它虽然不能应对接口受到恶意攻击(即突发请求),但较之第一种方式有个好处是,即使遭到攻击,也只会造成某个接口不可用,影响面小很多。实现同场景1。

3)限制接口每秒的请求数

这种方式和第二种很相似,只是它把时间窗口缩小到1秒,故而影响范围更小,实现如下:

// 存放令牌的缓存,失效时间为1s,缓存的key为当前时间秒值,value为这一秒对应的请求数,以当前时间秒值作为key的好处是方 
// 便统计一秒产生的请求总数,且下一秒到来时,数据会被自动清空
LoadingCache<Long, AtomicLong> tokenCache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.SECONDS).build(new CacheLoader<Long, AtomicLong>() {
      @Override
      public AtomicLong load(Long seconds) throws Exception {
           return new AtomicLong(0);
      }
});
// 每秒最大请求数
long limit = 200;
while(true) {
    Long currentSeconds = System.currentTimeMillis() / 1000;
    if(tokenCache.get(currentSeconds).incrementAndGet() > limit) {
        log.info("该请求被限流了");
        continue;
    }
    //do-business
}

4)平滑限流接口的请求数

平滑限流接口是为了对突发请求进行整形,以平均速率处理请求,故而可以应对突发请求,也是目前用的较多的限流方式。其分为平滑突发限流(SmoothBursty)、平滑预热限流(SmoothWarmingUp)两种模式,限流算法有令牌桶法、漏桶法。Google对Guava工具包提供了令牌桶的算法实现,使用起来也很简单、方便,下面会介绍令牌桶法、漏桶法。Guava实现限流如下:

RateLimiter limiter = RateLimiter.create(2);
System.out.println(limiter.acquire());

四、限流算法

1)令牌桶法

令牌桶法是以固定速率往桶里添加令牌,当令牌数达到上限时,便会丢弃或拒绝,其流程图如下:

令牌以固定速率放入桶中,如每秒放入r个; 桶中最多存放b个令牌,当桶中令牌数达到上限时,便丢弃或拒绝; 当需从桶中获取n个令牌时,若桶中令牌数大于n,便会删除桶里的n个令牌,否则请求等待。 2)漏桶法

漏桶法允许以任意速率流入桶内,但流出速率是恒定的,桶满了则丢弃令牌,桶内没有令牌时则阻塞,其流程图如下:

令牌以任意速率流入桶中; 以固定速率流出令牌,如果桶是空的,则不用流出; 若流入的令牌数超出了桶的容量,则令牌被丢弃。 由于令牌桶算法是以恒定速率流入桶中,令牌可以以任意速率流出,因而只需调整令牌流入速率便可解决流量突发情况,同时也支持预先消费,而漏桶算法是令牌以任意速率流入桶中,以恒定速率流出,从而不能解决流量突发情况(如常用的队列),如此对比令牌桶法相对漏桶法更优,但业务上不能一味的追求使用令牌桶法去实现,主要还需根据业务需求来断定使用那个限流算法。

五、总结

限流做为保护系统的一种常用方式,以限制系统的输入和输出流量已达到保护系统,业界也提供了很多现有的框架可直接使用,如jdk提供的Semaphore、google提供的Guava、Netflix提供的hystrix等,Semaphore是最简单粗暴的,直接使用计数器来控制。guava则采用了令牌桶法,可以平滑的进行限流。hystrix则提供了多种限流策略,使用起来更灵活,同时使用复杂度相对大一些,后续会介绍Semaphore、Guava、hystrix的实现原理。