Sentinel源码(三)slot解析

17 篇文章 2 订阅
15 篇文章 1 订阅

回顾

Sentinel源码(二)入口方法分析中,我们遗留了插槽链相关的内容,本文将详细解析这块内容

关于插槽链:在sentinel-core包下面默认提供了8个插槽连(com.alibaba.csp.sentinel.slotchain.ProcessorSlot)

# Sentinel default ProcessorSlots
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot


如果应用到自己的项目中,可以通过spi机制来扩展插槽链,比如添加一个MonitorSlot来加入自己公司内部的监控打点
另外在sentinel-parameter-flow-control中,还提供了一个com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowSlot

这些Slot通过标注的@SpiOrder进行排序
调用顺序如下图
在这里插入图片描述

NodeSelectorSlot

此类将尝试构建调用跟踪

  1. 如果需要,添加一个新的 DefaultNode 作为上下文中的最后一个子节点。上下文的最后一个节点是当前节点或上下文的父节点。
  2. 将自身设置为上下文当前节点。

代码举例

ContextUtil.enter("entrance1", "appA");
   Entry nodeA = SphU.entry("nodeA");
   if (nodeA != null) {
       nodeA.exit();
   }
   ContextUtil.exit();

上面的代码会在内存中生成如下调用结构:

在这里插入图片描述

这里的 EntranceNode 表示由 ContextUtil.enter(“entrance1”, “appA”) 给出的“entrance1”。 DefaultNode(nodeA) 和 ClusterNode(nodeA) 都持有“nodeA”的统计信息,由 SphU.entry(“nodeA”) 给出。 ClusterNode 由 ResourceId 唯一标识; DefaultNode 由资源 id 和上下文标识。换句话说,一个资源 id 将为每个不同的上下文生成多个 DefaultNode,但只有一个 ClusterNode

以下代码显示了两个不同上下文中的一个资源 ID:

ContextUtil.enter("entrance1", "appA");
      Entry nodeA = SphU.entry("nodeA");
      if (nodeA != null) {
          nodeA.exit();
      }
      ContextUtil.exit();
  
      ContextUtil.enter("entrance2", "appA");
      nodeA = SphU.entry("nodeA");
      if (nodeA != null) {
          nodeA.exit();
      }
      nodeB = SphU.entry("nodeB");
      if (nodeB != null) {
          nodeB.exit();
      }
      ContextUtil.exit();

上面的代码会在内存中生成如下调用结构:

在这里插入图片描述

我们也可以通过调用来检查这个结构:curl http://localhost:8719/tree?type=root

接下来查看enrty方法

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
		// 根据ContextName尝试获取DefaultNode
        DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                // 初始化DefaultNode,每个Context对应一个
                    node = new DefaultNode(resourceWrapper, null);
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;
                    // Build invocation tree
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }
		//将自身设置为上下文当前节点
        context.setCurNode(node);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

为什么使用上下文名称而不是资源名称作为映射键?

  1. 无论在哪个上下文中,相同的资源都将在全局范围内共享相同的 ProcessorSlotChain。因此,如果代码进入NodeSelectorSlot.entry方法,则资源名称必须相同,但上下文名称可能不同。
  2. 如果我们使用 com.alibaba.csp.sentinel.SphUentry(String resource)在不同的上下文中进入相同的资源,使用上下文名称作为映射键可以区分相同的资源。在这种情况下,将为每个不同的上下文(不同的上下文名称)创建多个具有相同资源名称的DefaultNode。

一个资源可能有多个 DefaultNode,那么获取同一资源总统计信息的最快方法是什么?

答案是所有具有相同资源名称的DefaultNode共享一个 ClusterNode。

ClusterBuilderSlot

此插槽维护资源运行统计信息(响应时间、qps、线程数、异常),以及由 ContextUtil.enter(String origin) 标记的调用者列表

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args)
        throws Throwable {
        //判断本资源是否已经初始化过clusterNode
        if (clusterNode == null) {
            synchronized (lock) {
                if (clusterNode == null) {
                    // Create the cluster node.
                    clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        node.setClusterNode(clusterNode);

        /*
         * if context origin is set, we should get or create a new {@link Node} of
         * the specific origin.
         */
        if (!"".equals(context.getOrigin())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
  1. clusterNodeMap是相同的资源共享一个 ClusterNode
private static volatile Map<ResourceWrapper, ClusterNode>clusterNodeMap = new HashMap<>();
  1. 判断本资源是否已经初始化过clusterNode,没有初始化则初始化clusterNode
  2. 给相同资源的DefaultNode设置一样的ClusterNode
  3. 如果没有来源则新建一个originNode

ClusterNode

  1. 此类存储资源的汇总运行时统计信息,包括 rt、线程数、qps 等。
  2. 同一个资源全局共享同一个 ClusterNode,无论在哪个 com.alibaba.csp.sentinel.context.Context 中。
  3. 为了区分来自不同来源的调用(在 ContextUtil.enter(String name, String origin) 中声明),一个 ClusterNode 持有一个 originCountMap,该映射持有不同来源的 StatisticNode。
  4. 使用 getOrCreateOriginNode(String) 获取特定来源的节点。请注意,“来源”通常是服务消费者的应用名称。

getOrCreateOriginNode方法

public Node getOrCreateOriginNode(String origin) {
        StatisticNode statisticNode = originCountMap.get(origin);
        if (statisticNode == null) {
            lock.lock();
            try {
                statisticNode = originCountMap.get(origin);
                if (statisticNode == null) {
                    // The node is absent, create a new node for the origin.
                    statisticNode = new StatisticNode();
                    HashMap<String, StatisticNode> newMap = new HashMap<>(originCountMap.size() + 1);
                    newMap.putAll(originCountMap);
                    newMap.put(origin, statisticNode);
                    originCountMap = newMap;
                }
            } finally {
                lock.unlock();
            }
        }
        return statisticNode;
    }

获取特定来源的节点。通常来源是服务消费者的应用程序名称。
如果给定原点的原点节点不存在,则将创建并返回原点的新StatisticNode。

相同的资源将全局共享相同的ProcessorSlotChain,无论在哪个上下文中。也就是说,能进入到同一个ClusterBuilderSlot对象的entry方法的请求都是来自同一个资源的,所以这些相同资源需要初始化一个统一的CluserNode用来做流量的汇总统计

LogSlot

对记录块异常的响应,以提供用于故障排除的具体日志。

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        try {
            fireEntry(context, resourceWrapper, obj, count, prioritized, args);
        } catch (BlockException e) {
            EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
                context.getOrigin(), count);
            throw e;
        } catch (Throwable e) {
            RecordLog.warn("Unexpected entry exception", e);
        }

    }

StatisticSlot

专用于实时统计的处理器插槽。进入这个slot时,我们需要单独统计以下信息:

  1. ClusterNode:资源ID的集群节点的总统计。
  2. Origin节点:来自不同callersorigins的集群节点的统计信息。
  3. DefaultNode:特定上下文中特定资源名称的统计信息。
  4. 最后是所有入口的总和统计。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // Do some checking.
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // Request passed, add thread count and pass count.
            node.increaseThreadNum();
            node.addPassRequest(count);

            if (context.getCurEntry().getOriginNode() != null) {
                // Add count for origin node.
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest(count);
            }

            // Handle pass event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        //......catch代码块先省略,后文单独介绍
    }

…catch代码块先省略,后文单独介绍

  1. 首先调用fireEntry方法进行后续插槽链的检查
  2. 后续检查全部通过后,增加线程数和通过的请求数
  3. 如果OriginNode不为空,则同样对其增加线程数和通过的请求数
  4. 如果EntryType为IN,记录全局ClusterNode线程数和通过的请求数
  5. 调用注册的回调方法。

其中第5步的回调是在ParamFlowStatisticSlotCallbackInit类中添加,ParamFlowStatisticEntryCallback和ParamFlowStatisticExitCallback用于热点参数线程级别的限流,修改统计指标中的线程计数器
Sentinel源码(六)ParamFlowSlot热点参数限流

public class ParamFlowStatisticSlotCallbackInit implements InitFunc {

    @Override
    public void init() {
        StatisticSlotCallbackRegistry.addEntryCallback(ParamFlowStatisticEntryCallback.class.getName(),
            new ParamFlowStatisticEntryCallback());
        StatisticSlotCallbackRegistry.addExitCallback(ParamFlowStatisticExitCallback.class.getName(),
            new ParamFlowStatisticExitCallback());
    }
}

但是这个init方法何时调用的呢?
在InitExecutor类中通过SPI机制

public static void doInit() {
        if (!initialized.compareAndSet(false, true)) {
            return;
        }
        try {
            ServiceLoader<InitFunc> loader = ServiceLoaderUtil.getServiceLoader(InitFunc.class);
            List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
            for (InitFunc initFunc : loader) {
                RecordLog.info("[InitExecutor] Found init func: " + initFunc.getClass().getCanonicalName());
                insertSorted(initList, initFunc);
            }
            for (OrderWrapper w : initList) {
                w.func.init();
                RecordLog.info(String.format("[InitExecutor] Executing %s with order %d",
                    w.func.getClass().getCanonicalName(), w.order));
            }
        } catch (Exception ex) {
            RecordLog.warn("[InitExecutor] WARN: Initialization failed", ex);
            ex.printStackTrace();
        } catch (Error error) {
            RecordLog.warn("[InitExecutor] ERROR: Initialization failed with fatal error", error);
            error.printStackTrace();
        }
    }

而doInit的调用则是在Env类初始化的时候。此类将触发 Sentinel 的所有初始化。

public class Env {

    public static final Sph sph = new CtSph();

    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }

}

注意:为防止死锁,其他类的静态代码块或静态字段不应引用此类

接下来发生异常的时候

catch (PriorityWaitException ex) {
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
                // Add count for origin node.
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseThreadNum();
            }
            // Handle pass event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        }

当发生PriorityWaitException的时候,只对DefaultNode和全局ClusterNode增加了线程数的记录,这是为什么呢?

这个在Sentinel源码(四)(滑动窗口流量统计)滑动窗口限流算法有详细解释,通过的线程数会在该算法中提前记录,所以catch的时候只增加线程数

接下来在看发生BlockException的时候

catch (BlockException e) {
            // Blocked, set block exception to current entry.
            context.getCurEntry().setBlockError(e);

            // Add block count.
            node.increaseBlockQps(count);
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseBlockQps(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseBlockQps(count);
            }

            // Handle block event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }

            throw e;
        }
  1. 对curEntry设置BlockError记录发生的异常
  2. Add block count包括defaultNode,originNode以及ClusterNode
  3. 调用注册的handler的onBlocked方法

看一下ENTRY_NODE是什么?

public final static ClusterNode ENTRY_NODE = new ClusterNode(TOTAL_IN_RESOURCE_NAME, ResourceTypeConstants.COMMON);

一个名字为__total_inbound_traffic__public final static String TOTAL_IN_RESOURCE_NAME = "__total_inbound_traffic__";类型为COMMON的ClusterNode,注意这个ClusterNode全局只有一个,是入站流量的全局统计节点。通常用于系统规则检查。
此处需要与ClusterBuilderSlot中的ClusterNode区别开来,后者是一个resource一个,前者是全局一个

AuthoritySlot

一个专用于AuthorityRule检查的ProcessorSlot。
简单点说就是对黑白名单的支持

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
        throws Throwable {
        checkBlackWhiteAuthority(resourceWrapper, context);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

核心方法就在checkBlackWhiteAuthority中

void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
        Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();

        if (authorityRules == null) {
            return;
        }

        Set<AuthorityRule> rules = authorityRules.get(resource.getName());
        if (rules == null) {
            return;
        }

        for (AuthorityRule rule : rules) {
            if (!AuthorityRuleChecker.passCheck(rule, context)) {
                throw new AuthorityException(context.getOrigin(), rule);
            }
        }
    }

根据资源获得对应的authorityRules,然后判断每一条规则,让我们看一下AuthorityRule详情


    /**
     * Mode: 0 for whitelist; 1 for blacklist.
     */
    private int strategy = RuleConstant.AUTHORITY_WHITE;

只有一个strategy表示0:白名单;1:黑明单
他的父类AbstractRule中还有这两个成员


    /**
     * Resource name.
     */
    private String resource;

    /**
     * <p>
     * Application name that will be limited by origin.
     * The default limitApp is {@code default}, which means allowing all origin apps.
     * </p>
     * <p>
     * For authority rules, multiple origin name can be separated with comma (',').
     * </p>
     */
    private String limitApp;

也就是对resource纬度配置黑白名单,limitApp就是resource的name,多个用逗号分隔即可

那么resource怎么解析呢?
实现RequestOriginParser类

public interface RequestOriginParser {

    /**
     * Parse the origin from given HTTP request.
     *
     * @param request HTTP request
     * @return parsed origin
     */
    String parseOrigin(HttpServletRequest request);
}

我们可以从request中解析出IP,user,appName等

最后看一下AuthorityRuleChecker的pasCheck方法

static boolean passCheck(AuthorityRule rule, Context context) {
        String requester = context.getOrigin();

        // Empty origin or empty limitApp will pass.
        if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
            return true;
        }

        // Do exact match with origin name.
        int pos = rule.getLimitApp().indexOf(requester);
        boolean contain = pos > -1;

        if (contain) {
            boolean exactlyMatch = false;
            String[] appArray = rule.getLimitApp().split(",");
            for (String app : appArray) {
                if (requester.equals(app)) {
                    exactlyMatch = true;
                    break;
                }
            }

            contain = exactlyMatch;
        }

        int strategy = rule.getStrategy();
        if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
            return false;
        }

        if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
            return false;
        }

        return true;
    }

代码很好理解:

  1. origin或者limitApp没有配置直接放行
  2. 根据origin判断是否在limitApp中配置过
  3. 如果策略是黑名单且配置了则阻拦
  4. 如果策略是白名单但是没有配置也进行拦截

SystemSlot

专用于 SystemRule 检查的 ProcessorSlot。

@Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        SystemRuleManager.checkSystem(resourceWrapper);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

查看唯一的方法checkSystem

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
        if (resourceWrapper == null) {
            return;
        }
        // Ensure the checking switch is on.
        if (!checkSystemStatus.get()) {
            return;
        }

        // for inbound traffic only
        if (resourceWrapper.getEntryType() != EntryType.IN) {
            return;
        }

        // total qps
        double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
        if (currentQps > qps) {
            throw new SystemBlockException(resourceWrapper.getName(), "qps");
        }

        // total thread
        int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
        if (currentThread > maxThread) {
            throw new SystemBlockException(resourceWrapper.getName(), "thread");
        }

        double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
        if (rt > maxRt) {
            throw new SystemBlockException(resourceWrapper.getName(), "rt");
        }

        // load. BBR algorithm.
        if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
            if (!checkBbr(currentThread)) {
                throw new SystemBlockException(resourceWrapper.getName(), "load");
            }
        }

        // cpu usage
        if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
            throw new SystemBlockException(resourceWrapper.getName(), "cpu");
        }
    }
  1. 确保检查开关已打开,默认不打开
  2. EntryType不是IN的直接返回
  3. 全局qps如果大于private static volatile double qps = Double.MAX_VALUE;抛出异常
  4. 全局线程数大于private static volatile long maxThread = Long.MAX_VALUE;抛出异常
  5. 全局平均响应大于private static volatile long maxRt = Long.MAX_VALUE;抛出异常
  6. 设置了系统最高负载且过载,抛出异常
  7. 设置了最高cpu使用率且超过,抛出异常

FlowSlot

这个是插槽链的核心:结合从之前的插槽(NodeSelectorSlot、ClusterNodeBuilderSlot 和 StatisticSlot)收集的运行时统计信息,FlowSlot 将使用预先设置的规则来决定是否应该阻止传入的请求。

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);
    }
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }

这个checker则是FlowRuleChecker

private final FlowRuleChecker checker;

而ruleProvider为

private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
        @Override
        public Collection<FlowRule> apply(String resource) {
            // Flow rule map should not be null.
            Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
            return flowRules.get(resource);
        }
    };
  1. 如果触发任何规则,SphU.entry(resourceName)将抛出FlowException。用户可以通过捕获 {@code FlowException} 自定义自己的逻辑。
  2. 一个资源可以有多个流规则。 FlowSlot 会遍历这些规则,直到其中一个被触发或所有规则都被遍历完。
  3. 每个FlowRule主要由这些因素组成:等级、策略、路径。我们可以结合这些因素来达到不同的效果。
  4. 等级由FlowRule中的 grade 字段定义。0 表示线程隔离,1 表示请求计数整形 (QPS)。线程数和请求数都是在实际运行时收集的,我们可以通过以下命令查看这些统计信息:curl http://localhost:8719/tree
  5. 这个阶段通常用于保护资源不被占用。如果资源需要很长时间才能完成,线程将开始占用。响应时间越长,占用的线程就越多。除了计数器,线程池或信号量也可以用来实现这一点。
  • 线程池:分配一个线程池来处理这些资源。当池中没有空闲线程时,请求被拒绝而不影响其他资源。
  • 信号量:使用信号量来控制该资源中线程的并发数。
  1. 使用线程池的好处是,它可以在超时时优雅地走开。但它也给我们带来了上下文切换和额外线程的成本。如果传入的请求已经在一个单独的线程中提供服务,例如,一个 Servlet HTTP 请求,如果使用线程池,它将几乎使线程数增加一倍。

当 QPS 超过阈值时,Sentinel 将采取行动控制传入的请求,由流规则中的 controlBehavior 字段配置。

  1. 直接拒绝:这是默认行为。超出的请求立即被拒绝并抛出 FlowException
  2. 预热:如果系统的负载已经低了一段时间,并且有大量的请求来了,系统可能无法一次处理所有这些请求。但是,如果我们不断增加传入的请求,系统可以预热并最终能够处理所有请求。可以通过在流规则中设置字段warmUpPeriodSec来配置此预热时间。
  3. 统一速率限制:该策略严格控制请求之间的间隔。换句话说,它允许请求以稳定、统一的速率通过。 该策略是漏桶的实现。它用于以稳定的速率处理请求,并且经常用于突发流量(例如消息处理)。当大量超出系统能力的请求同时到达时,采用这种策略的系统会按照固定的速率处理请求,直到所有请求都被处理完或超时

源码我们单独放Sentinel源码(五)FlowSlot以及限流控制器源码分析

DegradeSlot

这个slot主要针对资源的平均响应时间(RT)以及异常比率,来决定资源是否在接下来的时间被自动熔断掉。

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        performChecking(context, resourceWrapper);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

查看performChecking

void performChecking(Context context, ResourceWrapper r) throws BlockException {
        List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
        if (circuitBreakers == null || circuitBreakers.isEmpty()) {
            return;
        }
        for (CircuitBreaker cb : circuitBreakers) {
            if (!cb.tryPass(context)) {
                throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
            }
        }
    }

查看tryPass方法

public boolean tryPass(Context context) {
        // Template implementation.
        if (currentState.get() == State.CLOSED) {
            return true;
        }
        if (currentState.get() == State.OPEN) {
            // For half-open state we allow a request for probing.
            return retryTimeoutArrived() && fromOpenToHalfOpen(context);
        }
        return false;
    }

以下两种情况会通过请求

  1. 断路器关闭直接通过
  2. 断路器未打开,如果当前时间已经大于等于下次重试的时间戳并且断路器可以处于半开状态,则通过

那么就看看什么是半开状态

protected boolean fromOpenToHalfOpen(Context context) {
        if (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
            notifyObservers(State.OPEN, State.HALF_OPEN, null);
            Entry entry = context.getCurEntry();
            entry.whenTerminate(new BiConsumer<Context, Entry>() {
                @Override
                public void accept(Context context, Entry entry) {
                    // Note: This works as a temporary workaround for https://github.com/alibaba/Sentinel/issues/1638
                    // Without the hook, the circuit breaker won't recover from half-open state in some circumstances
                    // when the request is actually blocked by upcoming rules (not only degrade rules).
                    if (entry.getBlockError() != null) {
                        // Fallback to OPEN due to detecting request is blocked
                        currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                        notifyObservers(State.HALF_OPEN, State.OPEN, 1.0d);
                    }
                }
            });
            return true;
        }
        return false;
    }
  1. 尝试将State从OPEN变为HALF_OPEN并通过观察着模式进行事件回调
  2. 对CurEntry增加钩子函数,当请求实际上被即将到来的规则(不仅是降级规则)阻止时,确保断路器可以从半开状态恢复。

ParamFlowSlot

热点参数限流并非在 Sentinel 的 core 模块中实现的,而是在扩展模块中实现的。主要是根据同一资源不同的参数进行限流。

之前的限流策略都是针对资源维度的,热点参数限流则将维度细化到资源的某个参数上 同样的,详细内容之后会单独放一篇文章探究

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值