可能知道高并发系统需要限流这个东西,但具体是限制的什么,该如何去做,还是临摹两可。我们接下来系统性的给它归个小类,希望对你有所帮助。
google guava中提供了一个限流实现: RateLimiter,这个类设计的非常精巧,可以适用于我们日常业务中大多数流控
的场景,但鉴于使用场景的多样性,使用时也需要相当小心。
前面已经使用两篇简单的文章进行了预热。
信号量限流,高并发场景不得不说的秘密
没有预热,不叫高并发,叫并发高
这次不同。本篇文章将详细的,深入的介绍限流的各种场景和属性,然后分析guava这个限流器的核心源码,并对其特性进行总结。属于稍高级的进阶篇。
限流场景
弄清楚你要限制的资源,是这个过程中最重要的一环。我大体将它分为三类。
代理层
比如SLB、nginx或者业务层gateway等,都支持限流,通常是基于连接数
(或者并发数)、请求数
进行限流。限流的维度通常是基于比如IP地址、资源位置、用户标志等。更进一步,还可以根据自身负载情况动态调整限流的策略(基准)。
服务调用者
服务调用方,也可以叫做本地限流,客户端可以限制某个远端服务的调用速度,超过阈值,可以直接进行阻塞或者拒绝,是限流的协作方
。
服务接收方
基本同上,流量超过系统承载能力时,会直接拒绝服务。通常基于应用本身的可靠性考虑,属于限流的主体方
。我们常说的限流,一般发生在此处。本文主要结合RateLimiter讨论基于限流主体方的使用方式,其他的都类似。
限流策略
限流策略有时候很简单,有时候又很复杂,但常见的就三种。其他的都是在这之上进行的改进和扩展。
根据并发级别限流
这是一种简单的、易于实施的限流方式,可以使用我们前面提到的java信号量实现。它的使用场景也有着比较鲜明的特点:
1)每次请求,所需要的资源开支都比较均衡,比如,每个请求对CPU的消耗、IO消耗等,都差不多,请求的RT时间都基本接近。
2) 请求密度或稀疏或高频,这个我们不去关注。
3)资源本身不需要繁琐的初始化工作(预热
),或者初始化工作的开支可以忽略。(会增加复杂度)
4)对待流量溢出的策略比较简单,通常是直接拒绝
而不是等待,因为等待往往意味着故障。
这种策略通常在适用在流量的顶层组件上,比如代理层、中间件等对并发连接数的限制。而尝试获取凭证的超时时间,就叫做溢出等待
。很上档次很装b的词,对不对?
漏桶算法
请求流量以不确定速率申请资源,程序处理以恒定的速率进行,就是漏桶算法的基本原理。有点像制作冰激凌的过程。-.- 有关漏桶模型,大家可以去研究一下相关资料。
大体有以下几个概念。
桶 buffer
请求首先尝试进入队列,如果队列溢满,则拒绝此请求。进入队列以后,请求则等待执行。
由此可见,请求究竟何时被执行,还存在一些变数,这直接取决于队列中pending的请求数。有时候,挑剔的设计者会考虑增加有关限制请求等待的时间阈值,这个时间就是请求入队、出队的最大时差。buffer的大小设计,通常与速率有直接关系。
漏:请求出队
这个出队,有些讲究,不同的设计理念实现就有所不同。有抢占式
、有调度式
。其中“抢占式”就是处理线程(或者进程,比如nginx worker进程)在上一个请求处理完毕之后即从buffer队列中poll新的请求,无论当前线程(或者进程)的处理速率是否超过设定的速率,这种策略下buffer大小就限定了速率的上限。
调度式,就比较易于理解,需要额外的调度线程(进程),并严格按照设定的速率,从buffer中获取请求,并轮训的方式将请求交给其他worker线程,如果已有的worker线程繁忙,则直接创建新线程,目的就是确保速率是有保障的,这种方式下,buffer大小主要取决于等待时间。
溢出
就是因为漏桶的速率限制比较稳定,所以其面临流量突发(bursty)几乎没有应对能力,简单来说,超出buffer,就直接拒绝。
多么可怜的请求们。
流量突发
尽管buffer的设计在一定层面上兼顾流量突发,但是还是太脆弱了,比如某个瞬间,请求密度很高(最尴尬的就是,只大了一点),将buffer溢满,或许buffer再“大一点点”就能够在合理时间内被处理;对