Sentinel FlowSlot限流源码解析

本文主要来解析Sentinel限流核心源码,基于当前最新的release版本1.8.0

1、常见限流算法

1)、计数器算法

在指定周期内累加访问次数,当访问次数达到设定的阈值时,触发限流策略,当进入下一个时间周期清零访问次数

在这里插入图片描述

如上图所示,限定了每一分钟能够处理的总请求数为100,在第一个一分钟内,一共请求了60次。接着到第二个一分钟,counter又从0开始计数,在一分半钟时,已经达到了最大限流的阈值,这个时候后续的所有请求都会被拒绝

2)、滑动窗口算法

滑动窗口算法的原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可

在这里插入图片描述

如上图所示,将一分钟拆分成4个小时间窗口,每个小时间窗口最多能够处理25个请求。并且通过虚线框表示滑动窗口的大小(当前窗口的大小是2,也就是在这个窗口内最多能够处理50个请求)。同时滑动窗口会随着时间往前移动,比如前面1.5s结束之后,窗口会滑动到1.5~4.5s这个范围,然后在新的窗口中重新统计数据

Sentinel就是采用滑动窗口来实现限流的

3)、漏斗算法

漏斗算法主要作用是控制数据流入网络的速度,平滑网络上的突发流量

在这里插入图片描述

在漏斗算法内部维护一个容器,这个容器会以恒定速度出水,不管上面的水流速度多快,漏斗流出水的速度始终保持不变

在漏斗算法中,存在以下几种可能的情况:

  • 请求速度大于漏斗流出水的速度,也就是请求数超出当前服务所能处理的极限,将会触发限流策略
  • 请求速度小于或者等于漏斗流出水的速度,也就是服务端的处理能力满足客户端的请求量,将正常执行

漏斗算法是一种恒定速度的限流算法,无法处理短时间内的突发流量

4)、令牌桶算法

在这里插入图片描述

令牌桶算法对于每一个请求,都需要从令牌桶中获得一个令牌,如果没有获得令牌,则触发限流策略

系统会以一个恒定速度往固定容量的令牌桶中放入令牌,如果此时有客户端请求过来,则需要先从令牌桶中拿到令牌以获得访问资格

假设令牌桶生成速度是每秒10个,此时在请求获取令牌的时候,会存在以下三种情况:

  • 请求速度大于令牌生成速度,那么令牌会很快地取完,后续再进来的请求会被限流
  • 请求速度等于令牌生成速度,此时流量处于平稳状态
  • 请求速度小于令牌生成速度,说明此时系统的并发数并不高,请求能被正常处理

由于令牌桶有固定的大小,当请求速度小于令牌生成速度时,令牌桶会被填满。所以令牌桶能够处理突发流量,也就是在短时间内新增的流量系统能够正常处理

2、源码解析

Sentinel的熔断是由责任链中的FlowSlot来实现的

@SpiOrder(-2000)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    

    private final FlowRuleChecker checker;

    public FlowSlot() {
    
    
        this(new FlowRuleChecker());
    }

    FlowSlot(FlowRuleChecker checker) {
    
    
        AssertUtil.notNull(checker, "flow checker should not be null");
        this.checker = checker;
    }

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
    
    
      	//检查是否能够限流通过
        checkFlow(resourceWrapper, context, node, count, prioritized);
				//调用责任链下游的Slot的entry
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
    
    
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }

FlowSlot调用了FlowRuleChecker的checkFlow()方法:

public class FlowRuleChecker {
    
    

    public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    
    
        if (ruleProvider == null || resource == null) {
    
    
            return;
        }
      	//获取调用的resource对应的所有限流规则
        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
    
    
          	//逐个规则判断是否触发限流操作 触发限流,直接抛出FlowException
            for (FlowRule rule : rules) {
    
    
                if (!canPassCheck(rule, context, node, count, prioritized)) {
    
    
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
    }

    public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                                    boolean prioritized) {
    
    
        String limitApp = rule.getLimitApp();
        if (limitApp == null) {
    
    
            return true;
        }

      	//集群流控
        if (rule.isClusterMode()) {
    
    
            return passClusterCheck(rule, context, node, acquireCount, prioritized);
        }

      	//单机流控
        return passLocalCheck(rule, context, node, acquireCount, prioritized);
    }
  
    private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                          boolean prioritized) {
    
    
      	//根据请求选择节点
        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
        if (selectedNode == null) {
    
    
            return true;
        }

      	//根据配置FlowRule配置的controlBehavior(流控效果:直接拒绝、排队等待、慢启动模式)选择不同的Controller,判断是否通过
        return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
    }  

rule.getRater()返回的类型是TrafficShapingController,TrafficShapingController继承关系图如下:

在这里插入图片描述

下面主要来解析两种相对简单的策略:DefaultController和RateLimiterController

1)、DefaultController

在这里插入图片描述

在DefaultController中,首先获取当前的线程数或者QPS数,如果当前的线程数或者QPS+申请的数量>配置的总数,则不通过,如果当前线程数或者QPS+申请的数量<=配置的总数,则直接通过

public class DefaultController implements TrafficShapingController {
    
    

    private static final int DEFAULT_AVG_USED_TOKENS = 0;

    private double count;
    private int grade;

    public DefaultController(double count, int grade) {
    
    
        this.count = count;
        this.grade = grade;
    }

    @Override
    public boolean canPass(Node node, int acquireCount) {
    
    
        return canPass(node, acquireCount, false);
    }

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    
    
      	//获取当前node节点的线程数或者请求的qps总数
        int curCount = avgUsedTokens(node);
      	//当前请求数+申请总数是否>该资源配置的总数
        if (curCount + acquireCount > count) {
    
    
            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
    
    
                long currentTime;
                long waitInMs;
                currentTime = TimeUtil.currentTimeMillis();
                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
    
    
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    node.addOccupiedPass(acquireCount);
                    sleep(waitInMs);
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }

  	//获取当前node节点的线程数或者请求的qps总数
    private int avgUsedTokens(Node node) {
    
    
        if (node == null) {
    
    
            return DEFAULT_AVG_USED_TOKENS;
        }
        return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
    }

2)、RateLimiterController

在这里插入图片描述

RateLimiterControlle使用latestPassedTime属性来记录最后一次通过的时间,然后根据规则中QPS的限制,计算当前请求是否可以通过。它在Sentinel中的流控效果定义为排队等待

举个例子:设置QPS为10,那么每100毫秒允许通过一个,通过计算当前时间是否已经过了上一个请求的通过时间latestPassedTime之后的100毫秒,来判断是否可以通过。假设才过了50ms,那么需要当前线程再sleep 50ms,然后才可以通过。如果同时有另一个请求,那需要sleep 150ms

public class RateLimiterController implements TrafficShapingController {
    
    

  	//排队最大时长,默认500ms
    private final int maxQueueingTimeMs;
  	//QPS设置的值
    private final double count;
		//上一次请求通过的时间
    private final AtomicLong latestPassedTime = new AtomicLong(-1);

    public RateLimiterController(int timeOut, double count) {
    
    
        this.maxQueueingTimeMs = timeOut;
        this.count = count;
    }

    @Override
    public boolean canPass(Node node, int acquireCount) {
    
    
        return canPass(node, acquireCount, false);
    }

  	//通常acquireCount为1,prioritized为false
    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
    
    
        if (acquireCount <= 0) {
    
    
            return true;
        }
        if (count <= 0) {
    
    
            return false;
        }

        long currentTime = TimeUtil.currentTimeMillis();
        //计算每2个请求之间的间隔,比如QPS限制为10,那么间隔就是100ms
        long costTime = Math.round(1.0 * (acquireCount) / count * 1000);
				//预期本次请求的时间=时间间隔+上一个请求的通过时间
        long expectedTime = costTime + latestPassedTime.get();

      	//可以通过,设置latestPassedTime然后就返回true了
        if (expectedTime <= currentTime) {
    
    
            //设置上一个请求的通过时间 这里可能存在并发问题
          	//Contention may exist here, but it's okay.
            latestPassedTime.set(currentTime);
            return true;
        } else {
    
    
            //不可以通过,需要等待
            long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis();
          	//等待时长大于最大值,返回false
            if (waitTime > maxQueueingTimeMs) {
    
    
                return false;
            } else {
    
    
              	//将latestPassedTime往前推
                long oldTime = latestPassedTime.addAndGet(costTime);
                try {
    
    
                  	//需要sleep的时间
                    waitTime = oldTime - TimeUtil.currentTimeMillis();
                    if (waitTime > maxQueueingTimeMs) {
    
    
                        latestPassedTime.addAndGet(-costTime);
                        return false;
                    }
                    if (waitTime > 0) {
    
    
                        Thread.sleep(waitTime);
                    }
                    return true;
                } catch (InterruptedException e) {
    
    
                }
            }
        }
        return false;
    }

参考

【sentinel】深入浅出之原理篇FlowSlot

RateLimiter 源码分析(Guava 和 Sentinel 实现)

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/113172398