什么是接口限流算法
那么什么是限流呢?顾名思义,限流就是限制流量,包括并发的流量和必定时间内的总流量,就像你宽带包了1个G的流量,用完了就没了,因此控制你的使用频率和单次使用的总消耗。经过限流,咱们能够很好地控制系统的qps,从而达到保护系统或者接口服务器稳定的目的。服务器
接口限流的经常使用算法并发
计数器法this
计 数器法是限流算法里最简单也是最容易实现的一种算法。好比咱们规定,对于A接口来讲,咱们1分钟的访问次数不能超过100个。那么咱们能够这么作:在一开 始的时候,咱们能够设置一个计数器counter,每当一个请求过来的时候,counter就加1,若是counter的值大于100而且该请求与第一个 请求的间隔时间还在1分钟以内,那么说明请求数过多;若是该请求与第一个请求的间隔时间大于1分钟,且counter的值还在限流范围内,那么就重置 counter,具体算法的示意图以下:spa
伪代码以下code
class CounterDemo{ private $timeStamp; public $reqCount=0; public $limit=100;//时间窗口内最大请求数 public $interval=1000; //时间窗口 ms public function __construct() { $this->timeStamp = time(); } public function grant(){ $now=time(); if($now<$this->timeStamp+$this->interval){ //时间窗口内 $this->reqCount++; return $this->reqCount<=$this->limit; }else{ // 超时后重置 $this->timeStamp=time(); $this->reqCount=1; return true; } } }
这个算法看起来简单可是有个很严重的问题,那就是临界问题,看下图blog
从上图中咱们能够看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,而且1:00又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求。咱们刚才规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,用户经过在时间窗口的重置节点处突发请求, 能够瞬间超过咱们的速率限制。用户有可能经过算法的这个漏洞,瞬间压垮咱们的应用。token
聪明的朋友可能已经看出来了,刚才的问题实际上是由于咱们统计的精度过低。那么如何很好地处理这个问题呢?或者说,如何将临界问题的影响下降呢?咱们能够看下面的滑动窗口算法。接口
滑动窗口算法
直接上图吧图片
在上图中,整个红色的矩形框表示一个时间窗口,在咱们的例子中,一个时间窗口就是一分钟。而后咱们将时间窗口进行划分,好比图中,咱们就将滑动窗口 划成了6格,因此每格表明的是10秒钟。每过10秒钟,咱们的时间窗口就会往右滑动一格。每个格子都有本身独立的计数器counter,好比当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
那么滑动窗口怎么解决刚才的临界问题的呢?咱们能够看上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格 子中。当时间到达1:00时,咱们的窗口会往右移动一格,那么此时时间窗口内的总请求数量一共是200个,超过了限定的100个,因此此时可以检测出来触 发了限流。
我再来回顾一下刚才的计数器算法,咱们能够发现,计数器算法其实就是滑动窗口算法。只是它没有对时间窗口作进一步地划分,因此只有1格。
因而可知,当滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑,限流的统计就会越精确。
漏桶算法
漏桶算法,又称leaky bucket。为了理解漏桶算法,咱们看一下维基百科上的对于该算法的示意图:
从图中咱们能够看到,整个算法其实十分简单。首先,咱们有一个固定容量的桶,有水流进来,也有水流出 去。对于流进来的水来讲,咱们没法预计一共有多 少水会流进来,也没法预计水流的速度。可是对于流出去的水来讲,这个桶能够固定水流出的速率。并且,当桶满了以后,多余的水将会溢出。
咱们将算法中的水换成实际应用中的请求,咱们能够看到漏桶算法天生就限制了请求的速度。当使用了漏桶算法,咱们能够保证接口会以一个常速速率来处理请求。因此漏桶算法天生不会出现临界问题。具体的伪代码实现以下:
class LeakyDemo{ private $timeStamp; public $capacity;// 桶的容量 public $rate; // 水漏出的速度 public $water;// 当前水量(当前累积请求数) public function __construct() { $this->timeStamp = time(); } public function grant(){ $now=time(); $this->water=max(0,$this->water-($now-$this->timeStamp)*$this->rate);// 先执行漏水,计算剩余水量 $this->timeStamp=$now; if(($this->water+1)<$this->capacity){ // 尝试加水,而且水还未满 $this->water+=1; return true; }else{ // 水满,拒绝加水 return false; } } }
令牌桶算法
令牌桶算法,又称token bucket。为了理解该算法,咱们再来看一下维基百科上对该算法的示意图:
从图中咱们能够看到,令牌桶算法比漏桶算法稍显复杂。首先,咱们有一个固定容量的桶,桶里存放着令牌(token)。桶一开始是空的,token以 一个固定的速率r往桶里填充,直到达到桶的容量,多余的令牌将会被丢弃。每当一个请求过来时,就会尝试从桶里移除一个令牌,若是没有令牌的话,请求没法通 过。
具体的伪代码实现以下:
class TokenBucketDemo{ private $timeStamp; public $capacity;// 桶的容量 public $rate; // 令牌放入的速度 public $tokens;// 当前令牌的数量 public function __construct() { $this->timeStamp = time(); } public function grant(){ $now=time(); $this->tokens=min(0,$this->tokens+($now-$this->timeStamp)*$this->rate);// 先执行漏水,计算剩余水量 $this->timeStamp=$now; if($this->tokens<1){ // 若不到1个令牌,则拒绝 return false; }else{ // 还有令牌,领取令牌 $this->tokens -= 1; return true; } } }
临界问题
临界问题
咱们再来考虑一下临界问题的场景。在0:59秒的时候,因为桶内积满了100个token,因此这100个请求能够瞬间经过。可是因为token是以较低的 速率填充的,因此在1:00的时候,桶内的token数量不可能达到100个,那么此时不可能再有100个请求经过。因此令牌桶算法能够很好地解决临界问 题。下图比较了计数器(左)和令牌桶算法(右)在临界点的速率变化。咱们能够看到虽然令牌桶算法容许突发速率,可是下一个突发速率必需要等桶内有足够的 token后才能发生
总结
计数器算法是最简单的算法,能够当作是滑动窗口的低精度实现。滑动窗口因为须要存储多份的计数器(每个格子存一份),因此滑动窗口在实现上须要更多的存储空间。也就是说,若是滑动窗口的精度越高,须要的存储空间就越大。
漏桶算法 VS 令牌桶算法
漏桶算法和令牌桶算法最明显的区别是令牌桶算法容许流量必定程度的突发。由于默认的令牌桶算法,取走token是不须要耗费时间的,也就是说,假设桶内有100个token时,那么能够瞬间容许100个请求经过。
令牌桶算法因为实现简单,且容许某些流量的突发,对用户友好,因此被业界采用地较多。固然咱们须要具体状况具体分析,只有最合适的算法,没有最优的算法。