Sentinel笔记-Flow流控规则

Sentinel 之所以针对每个资源统计访问来源的指标数据,也是为了实现对丰富的限流策略的支持。

因为每个调用来源服务对同一个资源的访问频率都是不同的,针对调用来源限流可限制并发量较高的来源服务的请求,而对并发量低的来源服务的请求可不限流,或者是对一些并没有那么重要的来源服务限流。

当两个资源之间具有资源争抢关系的时候,使用 STRATEGY_RELATE 调用关系限流策略可避免多个资源之间过度的对同一资源争抢。例如查询订单信息和用户下单两个分别读和写数据库订单表的资源。

  

限流处理器插槽:FlowSlot

FlowSlot 是实现限流功能的切入点,它作为 ProcessorSlot 插入到 ProcessorSlotChain 链表中,在 entry 方法中调用 Checker 去判断是否需要拒绝当前请求,如果需要拒绝请求则抛出 Block 异常。FlowSlot 的源码如下:

public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    private final FlowRuleChecker checker;
    public FlowSlot() {
        this(new FlowRuleChecker());
    }

   // 规则生产者,一个 Function
    private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
        // 参数为资源名称
        @Override
        public Collection<FlowRule> apply(String resource) {
            Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
            return flowRules.get(resource);
        }
    };

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        checkFlow(resourceWrapper, context, node, count, prioritized);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
  // check 是否限流
    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}

限流规则检查器: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;
        }
        // (1)
        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
            // (2)
            for (FlowRule rule : rules) {
                // (3)
                if (!canPassCheck(rule, context, node, count, prioritized)) {
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
}

//canPassCheck 方法返回 true 说明允许请求通过,反之则不允许通过,
//其中check分为local check和 clusterCheck.
public boolean canPassCheck(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); }

其中 passLocalCheck包含三个关键步骤

  1. 根据调用来源和“调用关系限流策略”选择 DefaultNode
  2. 获取限流规则配置的流量效果控制器(TrafficShapingController);
  3. 调用 TrafficShapingController#canPass 方法完成 canPassCheck。

passLocalCheck 方法源码如下:

    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;
        }
        return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
    }

   //根据流控策略找到对应的实时统计信息(Node)
    static Node selectNodeByRequesterAndStrategy(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node) {

        // 限流规则针对的来源
        // 如果当前限流规则的 limitApp 为 default,则说明该限流规则对任何调用来源都生效,针对所有调用来源限流,否则只针对指定调用来源限流。
        String limitApp = rule.getLimitApp();

        //基于调用关系的限流策略
        int strategy = rule.getStrategy();

        // 调用来源, ContextUtil.enter(res,origin) 时传入        String origin = context.getOrigin();

        //如果限流规则配置的针对的调用方与当前请求实际调用来源匹配(并且不是 default、other)时的处理逻辑
        if (limitApp.equals(origin) && filterOrigin(origin)) {

            // (origin==limitApp),直接使用OriginNode, 实现针对该origin来源限流。
            if (strategy == RuleConstant.STRATEGY_DIRECT) {
                // Matches limit origin, return origin statistic node.
                return context.getOriginNode();
            }

            return selectReferenceNode(rule, context, node);

            //如果流控规则针对的调用方(limitApp) 配置的为 default,表示对所有的调用源都生效.

        } else if (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
            if (strategy == RuleConstant.STRATEGY_DIRECT) {
                // 直接返回 cluster node.
                return node.getClusterNode();
            }

            return selectReferenceNode(rule, context, node);

            //如果流控规则针对的调用方为(other),此时需要判断是否有针对当前的流控规则,只要存在,则这条规则对当前资源“失效”,
            //如果 limitApp 为 other,且该资源的所有限流规则都没有针对当前的Origin限流, 这使用本originNode
            //如果针对该资源没有配置其他额外的流控规则.
        } else if (RuleConstant.LIMIT_APP_OTHER.equals(limitApp)
                && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
            if (strategy == RuleConstant.STRATEGY_DIRECT) {
                return context.getOriginNode();
            }

            return selectReferenceNode(rule, context, node);
        }

        //都未匹配,表示没有找到Statistics Node,则直接通过.
        return null;
    }

    static Node selectReferenceNode(FlowRule rule, Context context, DefaultNode node) {
        String refResource = rule.getRefResource();
        int strategy = rule.getStrategy();

        if (StringUtil.isEmpty(refResource)) {
            return null;
        }

        //关联模式, 从集群环境中获取对应关联资源所代表的 Node
        //通俗点说就是使用其它资源的指标数据(statisticsNode)匹配当前的rule,
        //如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
        if (strategy == RuleConstant.STRATEGY_RELATE) {
            return ClusterBuilderSlot.getClusterNode(refResource);
        }

        // 判断当前调用上下文的入口资源与规则配置的是否一样,
        // 如果是,则返回入口资源对应的Node,即当前DefaultNode
        // 否则返回 null,表示该条流控规则,不参与流控判断.
        if (strategy == RuleConstant.STRATEGY_CHAIN) {
            if (!refResource.equals(context.getName())) {
                return null;
            }
            return node;
        }
        // No node.
        return null;
    }
  private static boolean filterOrigin(String origin) {
        //origin 不能为 default 和 other
        return !RuleConstant.LIMIT_APP_DEFAULT.equals(origin) && !RuleConstant.LIMIT_APP_OTHER.equals(origin);
    }
 

简而言之:

 
验证流程:就是逐个遍历Resource对应的Rule,拿Rule_limitApp去匹配来源origin(来源不能为default和other)
  1. 如果匹配上(limit_app==Origin),验证流控模式
    1. 直接流控:取origin_statistics_node
    2. 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
    3. 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
  2. 如果匹配不上,验证流控模式
    1. 如果Rule_limitApp为Deafult,
      1. 直接流控:取Cluster_statistics_node
      2. 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
      3. 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
    2. 如果Rule_limitApp为Other,并且该Res下没有对origin做单独规则,
      1. 直接流控:取origin_statistics_node
      2. 关联流控:通俗点说就是使用其它res的指标数据(statisticsNode)匹配当前的rule, 如果你的并发量高,到了我的规则值,我就限流,等你并发量降低到我的rule以下,我就不限流了;
      3. 链路模式:判断当前调用上下文的入口资源与规则配置的是否一样,如果是,则返回入口资源对应的Node,即当前DefaultNode,否则直接pass
    3. 标识规则没匹配上,直接pass 

从 selectNodeByRequesterAndStrategy 方法可以看出,Sentinel 之所以针对每个Resource统计访问来源的指标数据,也是为了实现对丰富的限流策略的支持。

因为每个调用来源服务对同一个资源的访问频率都是不同的,针对调用来源限流可限制并发量较高的来源服务的请求,而对并发量低的来源服务的请求可不限流,或者是对一些并没有那么重要的来源服务限流。

当两个资源之间具有资源争抢关系的时候,使用 STRATEGY_RELATE 调用关系限流策略可避免多个资源之间过度的对同一资源争抢。

流量效果控制器:TrafficShapingController

Sentinel 支持对超出限流阈值的流量采取效果控制器控制这些流量,流量效果控制支持:直接拒绝、Warm Up(冷启动)、匀速排队。

对应 FlowRule 中的 controlBehavior 字段。在调用 FlowRuleManager#loadRules 方法时,FlowRuleManager 会将限流规则配置的 controlBehavior 转为对应的 TrafficShapingController。

controlBehavior 的取值与使用的 TrafficShapingController 对应关系如下表格所示:

DefaultController

DefaultController 是默认使用的流量效果控制器,直接拒绝超出阈值的请求。当 QPS 超过限流规则配置的阈值,新的请求就会被立即拒绝,抛出 FlowException。

    @Override
    public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        // (1) 
        int curCount = avgUsedTokens(node);
        // (2)
        if (curCount + acquireCount > count) {
            // (3)
            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
                long currentTime;
                long waitInMs;
                currentTime = TimeUtil.currentTimeMillis();
                // (4)
                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                // (5)
                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                    // 将休眠之后对应的时间窗口的 pass(通过)这项指标数据的值加上 acquireCount
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    // 添加占用未来的 pass 指标的数量
                    node.addOccupiedPass(acquireCount);
                    // 休眠等待,当前线程阻塞
                    sleep(waitInMs);
                    // 抛出 PriorityWait 异常,表示当前请求是等待了 waitInMs 之后通过的
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }
  1. avgUsedTokens 方法:返回 node 当前时间窗口统计的QPS/ThreadCount,注意如果是ClusterNode的话,需要汇总所有Childer的QPS
  2. 如果将当前请求放行会超过限流阈值,且不满足(3),则直接拒绝当前请求。
  3. 如果限流阈值类型为 QPS,表示具有优先级的请求可以占用未来时间窗口的统计指标。
  4. 如果可以占用未来时间窗口的统计指标,则 tryOccupyNext 返回当前请求需要等待的时间,单位毫秒。
  5. 如果休眠时间在限制可占用的最大时间范围内,则挂起当前请求,当前线程休眠 waitInMs 毫秒。休眠结束后抛出 PriorityWait 异常,表示当前请求是等待了 waitInMs 之后通过的。

RateLimiterController

Sentinel 匀速流控效果是漏桶算法结合虚拟队列等待机制实现的,可理解为存在一个虚拟的队列,请求在队列中排队通过,每(count/1000)毫秒可通过一个请求,

要配置限流规则使用匀速通过效果控制器 RateLimiterController,则必须配置限流阈值类型为 GRADE_QPS,并且阈值要少于等于 1000

匀速流控适合用于请求突发性增长后剧降的场景。例如用在有定时任务调用的接口,在定时任务执行时请求量一下子飙高,但随后又没有请求的情况,这个时候我们不希望一下子让所有请求都通过,避免把系统压垮,但也不想直接拒绝超出阈值的请求,这种场景下使用匀速流控可以将突增的请求排队到低峰时执行,起到“削峰填谷”的效果。

WarmUpController

Warm Up,冷启动。在应用升级重启时,应用自身需要一个预热的过程,预热之后才能到达一个稳定的性能状态。Sentinel 冷启动限流算法参考了 Guava 的 SmoothRateLimiter 实现的冷启动限流算法。具体实现不做过多分析。

原文地址:https://www.cnblogs.com/snow-man/p/15505504.html