常见限流算法原理

Posted 张维鹏

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了常见限流算法原理相关的知识,希望对你有一定的参考价值。

        限流的目的是通过限制并发访问数或者时间窗口内的请求数,保证系统在流量突增情况下的稳定性,使系统不至于被高流量击垮,一旦达到限制阈值就可以拒绝服务(告知没有资源了或定向到错误页)、排队或等待(比如秒杀、评论、下单)、降级(返回兜底数据或默认数据,如商品详情页库存默认有货)。而一般高并发系统常见的限流方式有:

  • 限制单位时间的调用量
  • 限制系统的总并发数,比如数据库连接池、线程池
  • 限制时间窗口内的并发数,比如漏桶和令牌桶

下面我们就讨论各种限流方式的设计与具体应用场景

一、限制单位时间内的调用量:

        从字面上很容易理解,就是通过一个计数器来统计单位时间内某个服务的访问量。如果超过了阙值,则该单位时间段那不允许继续访问,或者把请求放入队列中等下一个时间段继续访问。需要注意的是计数器在进入下一个单位时间段时需要先清零。

        对于单位时间的设置,第一不能太长,太长将导致限流效果不够“敏感”;第二不能设置得太短,越短的单位时间段将导致阈值越难设置,比如1秒钟,因为高峰期的1秒钟和低峰期的1秒钟的流量有可能相差百倍甚至千倍。最优的单位时间片段应该以阈值设置的难易程度为标准,比如我们的监控系统统计的是服务每分钟的调用量,那我们自然可以选择1分钟作为时间片段,因为我们很容易评估每个服务在高峰期和低峰期的分钟调用量,并可以通过服务在每分钟的平均耗时和异常量来评估服务在不同单位时间段的服务质量。

        当单位时间段和阈值已经确定下来了,接下来就该考虑计数器的实现了,在 Java 中最常用的就是 AtomicLong,每次服务调用时,我们可以通过 AtomicLong.incrementAndGet() 方法来给计数器加1并返回最新值,并通过这个最新值和阈值来进行比较来看该服务单位时间段内是否超过了阈值。对于限制单位时间段内调用量的这种限流方式,实现简单,适用于大多数场景,但是也有一定的局限性,如果设定的阀值在单位时间段内的前几秒就被流量突刺消耗完了,将导致该时间段剩余的时间内该服务“拒绝服务”,这种现象被称为“突刺消耗”。

二、限制系统的总并发数:

        这种方式通过严格限制某服务的并发访问速度,也就限制了该服务单位时间段内的访问量。相比于第一种方案,它有着更严格的限制边界,因为如果采用第一种限流方案,如果大量的服务在极短的时间产生,仍然会压垮系统,甚至雪崩。并发限流一般用于服务资源有严格的限制的场景,比如连接数,线程数等。

        在 Java 中实现,并发限流也非常简单,比如可以使用信号量 Semaphore,在服务调用的入口调用非阻塞方法 Semaphore.tryAcquire() 来尝试获取一个信号量;如果获取失败,则直接返回或将调用放入到某个队列中;当服务执行完成,则使用 Semaphore.release() 来释放资源。

        但这种方式仍然可以造成流量尖刺,即每台服务器上该服务的并发量从 0 上升到阀值是没有任何阻力的,因为并发量考虑的只是服务能力边界的问题。

        

三、通过漏桶进行限流:

        漏桶算法有点像我们生活中用到的漏斗,液体倒进去漏斗后从下端小口中以固定速率流出。漏桶算法就是这样,不管流量有多大,漏桶都保证了流量的常速率输出,由于调用的消费速率已经固定,那么当桶的容量堆满时,就只能丢弃了。示意图如下:

         漏桶算法是一种悲观策略,它严格地限制了系统的吞吐量,从某种角度上来说,它的效果和并发量限流很类似。漏桶算法可以用于大多数场景,但是由于它对于服务吞吐量有着严格限制,可能导致某些服务称为瓶颈。

        在 Java 中想要实现一个漏桶算法,可以准备一个队列,当作桶的容量,另外通过一个计划线程池(ScheduledExecutorService)来定期从队列中获取并执行请求调用,可以一次拿多个请求,然后并发执行。

四、通过令牌桶进行限流:

        令牌桶算法可以看成是漏桶算法的一种改进,漏桶算法能够强行限制请求调用的速率,而令牌桶算法能够在限制平均调用速率的同时,允许一定程度的突发调用,实现平滑限流。令牌桶算法中,桶中会有一定数量的令牌,每次请求调用需要去桶中拿取令牌,拿到令牌后才有资格执行,否则必须等待。

        看到这里或许有些疑问,如果把令牌比喻成信号量,那么好像和并发限流没什么区别。其实不是,令牌桶算法的精髓在于拿令牌放令牌的方式,这和单纯的并发限流有明显的区别:

  • 每次请求时需要获取的令牌数不是固定的,比如当桶中的令牌比较多时,每次调用只需要获取一个令牌,随着令牌数的逐渐减少,当令牌使用率(使用中的令牌数/令牌总数)达到某个比率时,可能一次需要获取两个令牌,获取令牌的个数可能还会升高。

  • 归还令牌有两种方法,第一种是直接放回,第二种是什么也不做,由另一个额外的令牌生成步骤将令牌允许放回桶中。

         前面讲过,令牌桶允许一定程度的突发调用,那么关于令牌桶处理数据报文的方式,RFC 中定义了两种令牌桶算法:

  • 单速率三色标记(single rate three color marker,srTCM,RFC2697 定义,或称为单速双桶算法)算法,主要关注报文尺寸的突发。
  • 双速率三色标记(two rate three color marker,trTCM,RFC2698 定义,或称为双速双桶算法)算法,主要关注速率的突发。

        两种算法都是为报文打上红、黄、绿三种颜色的标记,所以称为“三色标记”。 QoS 会根据报文的颜色,设置报文的丢弃优先级,两种算法都可工作于色盲模式和非色盲模式。对于单速率三色标记算法和双速率三色标记算法感兴趣的读者,可以阅读这篇文章:https://zhuanlan.zhihu.com/p/164503398

小结:令牌桶和漏桶算法区别:

(1)内容上:令牌桶算法是按固定速率生成令牌,请求能从桶中拿到令牌就执行,否则执行失败。漏桶算法是任务进桶速率不限,但是出桶的速率是固定的,超出桶大小的的任务丢弃,也就是执行失败。

(2)突发流量适应性上:令牌桶限制的是流入的速率且灵活,允许一次取出多个 token,只要有令牌就可以处理任务,在桶容量上允许处理突发流量。而漏桶算法出桶的速率固定,有突发流量也只能按流出的速率处理任务,所以漏桶算法是平滑流入的速率,限制流出速率。

五、四种限流算法的比较与小结:

        下面给出在某种特定场景和特定参数下四种限流方式对服务并发能力影响的折线图,其中X轴表示当前并发调用数,而Y轴表示某服务在不同并发调用程度下采取限流后的实际并发调用数:

        在不同场景不同参数下,服务采用所述四种限流方式都会有不同的效果,没有哪种限流算法能保证在所有场景下都是最优限流算法,因为这需要从服务资源特性、限流策略(参数)配置难度、开发难度和效果检测难度等多方面因素来考虑。但是相比于其他三种限流方式来说,令牌桶算法限流无疑是最为灵活的,因为它有着众多可配置的参数来直接影响限流的效果,比如 Google 的 Guava 包的 RateLimiter 提供了令牌桶算法的实现,感兴趣的读者可以自行百度。

        最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,或者是其他限流算法,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量,这是合理的,如果因为极少部分流量需要保证的话,那么就可能导致系统达到极限而挂掉,得不偿失

参考文章:https://www.iteye.com/blog/manzhizhen-2311691

以上是关于常见限流算法原理的主要内容,如果未能解决你的问题,请参考以下文章

限流算法原理和实现

常见的限流算法与实现

四种限流算法原理

Resilience4j 源码解析:限流模块 RateLimiter 与 常见限流算法

Resilience4j 源码解析:限流模块 RateLimiter 与 常见限流算法

Sentinel滑动时间窗限流算法原理及源码解析(下)