拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 限流常见方案

限流常见方案

白鹭 - 2022-03-01 2164 0 0

限流常见方案

 

            我歌月徘徊,我舞影零乱,
    醒时相交欢,醉后各分散,

 

一、限流思路

常见的系统服务限流模式有:熔断、服务降级、延迟处理和特殊处理四种,

1、熔断

将熔断措施嵌入到系统设计中,当系统出现问题时,若短时间内无法修复,系统会自动开启熔断开关,拒绝流量访问,避免大流量对后端的过载请求,

除此之外,系统还能够动态监测后端程序的修复情况,当程序已恢复稳定时,就关闭熔断开关,恢复正常服务,

常见的熔断组件有 Hystrix 以及阿里的 Sentinel,

在Spring Cloud框架里,熔断机制通过Hystrix实作,Hystrix会监控微服务间呼叫的状况,当失败的呼叫到一定阈值,预设是5秒内20次呼叫失败,就会启动熔断机制, 熔断机制的注解是@HystrixCommand,Hystrix会找有这个注解的方法,并将这类方法关联到和熔断器连在一起的代理上,

2、服务降级

将系统的所有功能服务进行一个分级,当系统出现问题需要紧急限流时,可将不是那么重要的功能进行降级处理,停止服务,保障核心功能正常运作,

例如在电商平台中,如果突发流量激增,可临时将商品评论、积分等非核心功能进行降级,停止这些服务,释放出机器和 CPU 等资源来保障用户正常下单,

这些降级的功能服务可以等整个系统恢复正常后,再来启动,进行补单/补偿处理,

除了功能降级以外,还可以采用不直接操作数据库,而全部读快取、写快取的方式作为临时降级方案,

熔断&降级

  • 相同点:

    目标一致 都是从可用性和可靠性出发,为了防止系统崩溃;

    用户体验类似,最终都让用户体验到的是某些功能暂时不可用,

  • 不同点:

    触发原因不同,服务熔断一般是某个服务(下游服务,即被呼叫的服务)故障引起;

  • 而服务降级一般是从整体负荷考虑,

3、延迟处理

延迟处理需要在系统的前端设定一个流量缓冲池,将所有的请求全部缓冲进这个池子,不立即处理,后端真正的业务处理程序从这个池子中取出请求依次处理,常见的可以用队列模式来实作,

这就相当于用异步的方式去减少了后端的处理压力,但是当流量较大时,后端的处理能力有限,缓冲池里的请求可能处理不及时,会有一定程度延迟,

4、特权处理

这个模式需要将用户进行分类,通过预设的分类,让系统优先处理需要高保障的用户群体,其它用户群的请求就会延迟处理或者直接不处理,

二、限流算法

常见的限流算法有三类:计数器算法、漏桶算法和令牌桶算法,

1、计数器算法

计数器算法是限流算法中最简单最容易的一种,如上图每分钟只允许100个请求,第一个请求进去的时间为startTime,在startTime + 60s内只允许100个请求 ,

当60s内超过十个请求后,则拒绝请求;不超过的允许请求,到第60s 则重新设定时间,

 1 package com.todaytalents.rcn.parser.util;
 2 
 3 import java.util.concurrent.atomic.AtomicInteger;
 4 
 5 /**
 6  * 计数器实作限流:
 7  * 每分钟只允许100个请求,第一个请求进去的时间为startTime,在startTime + 60s内只允许100个请求
 8  * 60s内超过100个请求后,则拒绝请求,
 9  * 不超过,允许请求,到第60s 重新设定时间,
10  *
11  * @author: Arafat
12  * @date: 2021/12/29
13  * @company: 澳B99999
14  **/
15 public class CalculatorCurrentLimiting {
16 
17     /**
18      * 限流个数
19      */
20     private int maxCount = 100;
21     /**
22      * 指定的时间内:秒
23      */
24     private long specifiedTime = 60;
25     /**
26      * 原子类计数器
27      */
28     private AtomicInteger atomicInteger = new AtomicInteger(0);
29     /**
30      * 起始时间
31      */
32     private long startTime = System.currentTimeMillis();
33 
34     /**
35      * @param maxCount      限流个数
36      * @param specifiedTime 指定的时间内
37      * @return 回传true 不限流,回传false 则限流
38      */
39     public boolean limit(int maxCount, int specifiedTime) {
40         atomicInteger.addAndGet(1);
41         if (1 == atomicInteger.get()) {
42             startTime = System.currentTimeMillis();
43             atomicInteger.addAndGet(1);
44             return true;
45         }
46         // 超过时间间隔,重新开始计数
47         if (System.currentTimeMillis() - startTime > specifiedTime * 1000) {
48             startTime = System.currentTimeMillis();
49             atomicInteger.set(1);
50             return true;
51         }
52         // 还在时间间隔内,检查是否超过限流数量
53         if (maxCount < atomicInteger.get()) {
54             return false;
55         }
56         return true;
57     }
58 
59 }
View Code

利用计数器算法比如要求某一个界面,1分钟内的请求不能超过100次,

可以在开始时设定一个计数器,每次请求,该计数器+1;如果该计数器的值大于10并且与第一次请求的时间间隔在1分钟内,那么说明请求过多则限制请求直接回传或不处理,反之,

如果该请求与第一次请求的时间间隔大于1分钟,并且该计数器的值还在限流范围内,那么重置该计数器,

计算器算法虽然简单,但它有一个狠致命的临界问题,

上图可以看出假若有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且在1:00时,又瞬间发送了100个请求,那么其实这个用户在 1秒里面,瞬间发送了200个请求,

而上述计数器算法规定的是1分钟最多100个请求,也就是每秒钟最多1.7个请求,而用户通过在时间视窗的重置节点处突发请求,可以瞬间超过限流的速率限制,这个漏洞可能会瞬间压垮服务应用,

上述漏洞问题其实是因为计数器限流算法统计的精度太低,可以借助滑动视窗算法将临界问题的影响降低,

2、滑动视窗

上图中,整个红色的矩形框表示一个时间视窗,在计数器算法限流的例子中,一个时间视窗就是一分钟,在这里将时间视窗进行划分,比如图中,将滑动视窗划成了6格,每格代表的是10秒钟,每过10秒钟,时间视窗就会往右滑动一格,每一个格子都有自己独立的计数器counter,比如当一个请求在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1,

那么滑动视窗怎么解决刚才的临界问题的呢?

上图,0:59到达的100个请求会落在灰色的格子中,而1:00到达的请求会落在橘黄色的格子中,当时间到达1:00时,视窗会往右移动一格,那么此时时间视窗内的总请求数量一共是200个,超过了限定的100个,所以此时能够检测出来触发了限流,

经比较发现发现,计数器算法其实就是滑动视窗算法,只是它没有对时间视窗做进一步地划分,所以只有1格,所以,当滑动视窗的格子划分的越多,则滑动视窗的滚动就越平滑,限流的统计就会越精确,

3、漏桶算法

漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会超过桶可接纳的容量时直接溢位,可以看出漏桶算法能强行限制资料的传输速率,

使用漏桶算法,可以保证界面会以一个常速速率来处理请求,所以漏桶算法必定不会出现临界问题,

漏桶算法实作类:

 1 import java.util.concurrent.atomic.AtomicInteger;
 2 
 3 /**
 4  * 漏桶算法:把水滴看成请求
 5  *
 6  * @author: Arafat
 7  * @date: 2021/12/29
 8  **/
 9 public class LeakyBucket {
10     /**
11      * 桶的容量
12      */
13     private int capacity = 100;
14     /**
15      * 桶剩余的水滴的量(初始化的时候桶为空)
16      */
17     private AtomicInteger water = new AtomicInteger(0);
18     /**
19      * 水滴的流出的速率 每1000毫秒流出1滴
20      */
21     private int leakRate;
22     /**
23      * 第一次请求之后,木桶在这个时间点开始漏水
24      */
25     private long leakTimeStamp;
26 
27     public LeakyBucket(int leakRate) {
28         this.leakRate = leakRate;
29     }
30 
31     public boolean acquire() {
32         // 如果是空桶,就用当前时间作为桶开始漏出的时间
33         if (water.get() == 0) {
34             leakTimeStamp = System.currentTimeMillis();
35             water.addAndGet(1);
36             return capacity == 0 ? false : true;
37         }
38         // 先执行漏水,计算剩余水量
39         int waterLeft = water.get() - ((int) ((System.currentTimeMillis() - leakTimeStamp) / 1000)) * leakRate;
40         water.set(Math.max(0, waterLeft));
41         // 重新更新leakTimeStamp
42         leakTimeStamp = System.currentTimeMillis();
43         // 尝试加水,并且水还未满
44         if ((water.get()) < capacity) {
45             water.addAndGet(1);
46             return true;
47         } else {
48             // 水满,拒绝加水,直接溢位
49             return false;
50         }
51     }
52     
53 }
View Code

使用漏桶限流:

 1 /**
 2  * @author Arafat
 3  */
 4 @Slf4j
 5 @RestController
 6 @AllArgsConstructor
 7 @RequestMapping("/test")
 8 public class TestController {
 9 
10     /**
11      * 漏桶:水滴的漏出速率是每秒 1 滴
12      */
13     private LeakyBucket leakyBucket = new LeakyBucket(1);
14 
15     private UserService userService;
16 
17     /**
18      * 漏桶限流
19      *
20      * @return
21      */
22     @RequestMapping("/searchUserInfoByLeakyBucket")
23     public Object searchUserInfoByLeakyBucket() {
24         // 限流判断
25         boolean acquire = leakyBucket.acquire();
26         if (!acquire) {
27             log.info("请您稍后再试!");
28             return Reply.success("请您稍后再试!");
29         }
30         // 若没有达到限流的要求,直接呼叫界面查询
31         return Reply.success(userService.search());
32     }
33 
34 }
View Code

漏桶算法的两个优点:

  • 削峰:有大量流量进入时,会发生溢位,从而限流保护服务可用,
  • 缓冲:不至于直接请求到服务器,缓冲压力,消费速度固定,因为计算性能固定,

4、令牌桶算法

令牌桶算法思想:以固定速率产生令牌,放入令牌桶,每次用户请求都得申请令牌,令牌不足则拒绝请求或等待,

上图,令牌桶算法会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务, 

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *