一、前言
对于一个零碎而言,最重要的要求之一必定就是服务的稳定性了,一个不稳固的零碎可能给企业带来微小的损失,包含经济和品牌等等方面的损失。
咱们都心愿零碎能稳固牢靠地对外提供服务,响应快,不宕机,不故障,然而在理论状况中,经常会遇到一些异样的状况,这就考验咱们零碎的稳定性了。
明天就来讲讲保障服务稳定性的伎俩之一的服务限流。
二、解决的问题
咱们零碎运行过程中有时候可能会遇到突发异样大流量,如果零碎无奈正确处理这些忽然涌入大量申请,就会导致系统轻则响应慢,常常超时,重则导致整个零碎宕机,因而这就要求咱们零碎能以肯定的策略解决大流量的涌入,这样才不对被突发大流量压垮,导致齐全无奈对外提供服务。
留神,这里大流量说的是突发异样大流量,是非正常状况,所以咱们的解决策略是对局部申请进行抛弃或者排队解决,保障咱们零碎对外还是可用的,而非全副申请都须要解决完。而对于零碎失常的一般流量来说,如果其申请量逐步达到了咱们零碎的负载能力的下限的话,这时候须要进行的就是服务的扩容,而不是限流并抛弃申请了。
咱们零碎可能遇到的突发大流量的场景有很多,但对立的体现都是,某些接口申请量激增,导致超过了零碎的解决能力了,例如:
- 突发热点事件(例如微博热搜)
- 爬虫
- 歹意攻打
- 歹意刷单(如12306)
···
面对突发大流量,咱们零碎能应用的伎俩之一就是服务限流了。限流是通过对一个时间段解决内的申请量进行限度来爱护零碎,一旦达到限度速率则能够抛弃申请,从而管制了零碎解决的申请量不会超过其解决能力。
限流可能在整个网络申请过程的各个层面产生,例如nginx,业务代码层等,这里次要介绍的是限流的思维,也就是限流算法,并给出业务层的代码实现例子。
三、限流算法
1、计数器算法
计数器限流算法是比较简单粗犷的算法,次要通过一个或者多个计数器来统计一段时间内的申请总量,而后判断是否超过限度,超过则进行限流,不超过则对应的计数器数量加1。
计数器限流算法又能够分为固定窗口和滑动窗口两种。
固定窗口
固定窗口计数器限流算法是统计固定一个工夫窗口内的申请总数来判断是否进行限流,例如限度每分钟申请总数为100,则能够通过一个计数器来统计以后分钟的申请总数,每来一个申请判断以后分钟对应的计数器的数量,没有超过限度则在以后分钟对应的计数器加1,超过则拒绝请求。
PHP实现逻辑如下:
/** * 固定窗口计数器限流算法 * @param $key string 限流根据,例如uid,url等 * @param $time int 限流时间段,单位秒 * @param $limit int 限流总数 */function limit($key, $time, $limit) { //以后工夫所在分片 $current_segment=floor(time() / $time); //按以后工夫和限流参数生成key $current_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment; $redis = new Redis(); //key不存在才设置,且设置过期工夫 $redis->set($current_key, 0, ['nx', 'ex' => $time]); $current = $redis->incr($current_key); //为了解决申请并发的问题,代码实现上要先加再判断 if ($current > $limit) { return false; } return true;}
毛病
固定窗口计数器限流算法实现起来尽管很简略,然而有一个非常致命的问题,那就是临界突刺问题:最初一秒和最开始1秒的流量集中一起,会呈现大量流量。
计数器的限度数量判断是按时间段的,在两个时间段的接壤工夫点,限度数量的以后值会产生一个抖动的变动,从而使霎时流量冲破了咱们冀望的限度。例如以下的状况:
能够看到在0:59的时候,如果忽然来了100个申请,这时候以后值是100,而到了1:00的时候,因为是下一个时间段了,以后值陡降到0,这时候又进来100个申请,都能通过限流判断,尽管两个时间段均匀下来还是没超过限度,然而在临界工夫点的申请量却达到了两倍之多,这种状况下就可能压垮咱们的零碎。
滑动窗口
下面会呈现突刺的问题其实就在于固定窗口算法的窗口时间跨度太大,且是固定不变的,为了解决突刺的问题,咱们就有了滑动窗口计数器限流算法。
滑动窗口算法是固定窗口算法的优化版,次要有两个特点:
- 划分多个小的时间段,各时间段各自进行计数。
- 依据以后工夫,动静往前滑动来计算工夫窗口范畴,合并计算总数。
能够看到,每次工夫往后,窗口也会动静往后滑动,从而抛弃一些更晚期的计数数据,从而实现总体计数的安稳适度。当滑动窗口划分的格子越多,那么滑动窗口的滑动就越平滑,限流的统计就会越准确。事实上,固定窗口算法就是只划分成一个格子的滑动窗口算法。
PHP实现逻辑如下:
/** * 滑动窗口计数器限流算法 * @param $key string 限流根据,例如uid,url等 * @param $time int 限流时间段,单位秒 * @param $limit int 限流总数 * @param $segments_num int 分段个数 */function limit($key, $time, $limit, $segments_num) { //小分片工夫长度 $segments_time=floor($time/$segments_num); //以后工夫所在小分片 $current_segment=floor(time() / $segments_time); //按限流时间段生成key $current_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment; $redis = new Redis(); //先更新以后工夫所在小分片计数,key不存在才设置,且设置过期工夫 $redis->set($current_key, 0, ['nx', 'ex' => $time]); $current = $redis->incr($current_key); for($window=$segments_time;$window<$time;$window+=$segments_time){ $current_segment=$current_segment-1; $tmp_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment; //计算工夫窗口内的总数 $current+=intval($redis->get($tmp_key)); if ($current > $limit) { //超过限度的话要回滚本次加的次数 $redis->decr($current_key); return false; } } return true;}
毛病
滑动窗口限流算法尽管能够保障任意工夫窗口内接口申请次数都不会超过最大限流值,然而相对来说对系统的刹时解决能力还是没有思考到,无奈避免在更细的工夫粒度上拜访过于集中的问题,例如在同一时刻(同一秒)大量申请涌入,还是可能会超过零碎负荷能力。
2、漏桶算法
漏桶算法就是一种从零碎的解决能力登程进行限流的算法。相似生存用到的漏斗,下面进水,上面有个小口出水,当申请进来时,相当于水倒入漏斗,而后从下端小口缓缓匀速的流出。不论下面流量多大,上面流出的速度始终保持不变,超过漏斗容量的则抛弃。漏桶算法以固定的速率开释拜访申请(即申请通过),直到漏桶为空。
漏桶算法有两个要害数据:桶的容量V和流出的速率R。假如每个申请的均匀解决工夫是S,最大超时工夫是SS,则V/R+S<=SS。
能够应用队列来实现,队列设置最大容量,拜访申请先进入队列,队列满了的话就抛弃抛弃后续申请,而后通过另外一个woker以固定速率从队列进口拿申请去解决。具体实现逻辑不展现了。
毛病
漏桶算法的缺点很显著,因为进口的解决速率是固定的,当短时间内有大量的突发申请时,即使此时服务器没有任何负载,每个申请也都得在队列中期待一段时间能力被响应,因而漏桶算法无奈应答突发流量。
3、令牌桶算法
令牌桶算法也是有一个桶,然而它不是通过限度漏出的申请来管制流量,而是通过管制桶的令牌的生成数量来达到限流的目标的。令牌桶定时往桶外面丢肯定的令牌,令牌桶满了就不再往里面加令牌。每来一个申请就要先在桶里拿一个令牌,拿到令牌则通过,拿不到则回绝。
当访问量小于令牌桶的生成速率时,令牌桶能够缓缓积攒令牌直到桶满,这样当短时间的突发访问量来时,其积攒的令牌数保障了大量申请都能立即拿到令牌进行后续解决。当访问量继续大量流入时,积攒的令牌被耗费完了之后,后续申请又依赖于肯定速率产生的新令牌,这时候就变成相似漏桶算法那样的固定流量限度。
由此可见,相比于漏桶算法,令牌桶算法可能在限度调用的均匀速率的同时还容许肯定水平的突发调用。
PHP实现逻辑如下:
/** * 令牌桶限流算法 * @param $key string 限流根据,例如uid,url等 * @param $rate float 令牌生成速率,每秒$rate个 * @param $volume int 容量 * @return bool */function limit($key, $rate, $volume) { //按限流参数生成key $current_key = $key . '_' . $rate . '_' . $volume; $redis = new Redis(); $time=time(); //没有则初始化 $redis->hSetNx($current_key, 'num', $volume); $redis->hSetNx($current_key, 'time', $time); //以下逻辑在高并发状况下要用lua脚本或者加分布式锁,这里仅用于阐明算法的逻辑,就不思考并发状况了 //计算从上次到当初,须要增加的令牌数量 $last=$redis->hMGet($current_key,['num','time']); $last_time=$last['time']; $last_num=$last['num']; $incr=($time-$last_time)*$rate; $current=min($volume,($last_num+$incr));//计算以后令牌数 if($current>0){ $redis->hMSet($current_key,[ 'num'=>$current-1, 'time'=>time() ]); return true; } return false;}
下面的实现计划是令牌按工夫回复数量,事实上令牌的生成也能够通过另外的服务去生成,这样能够按肯定策略去调控令牌的生成速率。
版权申明
转载请注明作者和文章出处
作者: X学生
https://segmentfault.com/a/1190000023411052