Sentinel的流程原理


以下有很多部分有参考自sentinel的官方文档,做一个笔记,顺便加深自己的理解。
同时建议参照ProcessOn上的流程图来配合看源码,会更容易理解。
附上一个地址:链接: Sentinel原理图.

1、Sentinel的原理图

这是sentinel官方的介绍:
在这里插入图片描述

2、Sentinel的概念介绍

在 Sentinel 里面,所有的资源都对应一个资源名称(resourceName),每次资源调用都会创建一个 Entry 对象。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建。Entry 创建的时候,同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责,例如:

  • NodeSelectorSlot 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot 则通过系统的状态,例如 load1 等,来控制总的入口流量;

Context、Entrance Node、DefaultNode和ClusterNode之间的关系:

  • 首先这个NodeSelectorSlot 会去构建图上所示的Invocation Tree,生成Machine Root,Entrance Node和DefaultNode。
  • 每一个上下文Context就会对应一个入口节点Entrance Node。
  • 每一个资源都会有一个DefaultNode节点用于统计资源在当前节点的流量数据。
  • 集群节点ClusterNode用于统计一个资源在所有上下文Context中的信息。

3、那么下面就跟着源码看看大致的运行流程:

Sentinel可以通过注解或者使用代码的方式去声明一个资源,那我们就从注解的方式开始看起吧。在Sentinel中,有一个切面SentinelResourceAspect,是用于截取注解了@SentinelResource的方法的。
在里面有一个环绕通知的方法invokeResourceWithSentinel用于增强原方法。
在该增强方法中所做的事情:

  1. 获取注解中的资源名,入口类型等信息
  2. 调用SphU.entry去获取一个token,所有的数据统计和规则检查都是在这个方法里面进行的
  3. 执行原资源的目标方法
  4. 如果一些规则不通过,处理异常,如执行BlockHandler程序。
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
    //找到被注解了@SentinelResource的方法作为切点
    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }
    //采用环绕通知,对该方法做增强处理
    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        Method originMethod = resolveMethod(pjp);

        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();
        Entry entry = null;
        try {
            //每一次调用资源都会去创建一个entry
            //所有的数据统计和规则检查都是在这个方法里面进行的
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            //能顺利的执行完这个方法就说明成功的通过了所有的规则检查。
            //否则是会抛出异常在下面进行捕获的,到不了这里
            Object result = pjp.proceed();//可以执行原目标资源的方法
            return result;
        } catch (BlockException ex) {
            //发生阻塞异常了,就进入到该资源指定的BlockHandler程序。
            //如果找不到BlockHandler程序,就会走降级机制
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                return handleFallback(pjp, annotation, ex);
            }

            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }
}

在这里基本所有的业务逻辑都在SphU.entry里,所以下面就看这个方法,它最终会调用entryWithPriority方法,做了下面这些事情:

  1. 尝试获取当前线程对应的上下文
  2. context数量已经超出阈值就返回不带规则检查的CtEntry
  3. 没有获取到上下文就创建一个默认的上下文
  4. 查询该资源需要调用的处理槽链
  5. slotchain数量超出阈值,也会返回不带规则检查的CtEntry
  6. 进入插槽调用链路,做规则检查等操作
    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
            throws BlockException {
        //尝试获取当前线程对应的上下文
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
            // so here init the entry only. No rule checking will be done.
            //如果是NullContext的话就表示当前context数量已经超出阈值了,所以仅仅返回一个不带slotchain的CtEntry,不会进行任何的规则检查
            return new CtEntry(resourceWrapper, null, context);
        }

        if (context == null) {
            // Using default context.
            //默认会创建一个叫sentinel_default_context的context
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        // Global switch is close, no rule checking will do.
        //这是sentinel的全局开关,如果是关闭状态就不会做任何的规则检查
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }
        //在chainMap查询该资源需要调用的处理槽链,如果没有找到就创建一个并放到chainMap里,这里返回的只是第一个调用的插槽
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        /*
         * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * so no rule checking will be done.
         */
        if (chain == null) {
            //如果目前的插槽链在map里的数目超出阈值了,这里chain就会是空的,那就直接返回一个不带任何规则检查的CtEntry
            return new CtEntry(resourceWrapper, null, context);
        }

        Entry e = new CtEntry(resourceWrapper, chain, context);
        try {
            //开始进入调用链路,做规则检查等操作
            chain.entry(context, resourceWrapper, null, count, prioritized, args);
        } catch (BlockException e1) {
            e.exit(count, args);
            throw e1;
        } catch (Throwable e1) {
            // This should not happen, unless there are errors existing in Sentinel internal.
            RecordLog.info("Sentinel unexpected exception", e1);
        }
        return e;
    }

在entryWithPriority方法中,生成调用链的方法是lookProcessChain,如果没有获取到该资源的调用链,它会调用slotChainBuilder.build()方法,根据SPI机制去加载的。并且会去轮询将这些插槽一个一个添加到末尾形成一条调用链路。

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        //通过spi机制加载所有的排好序的插槽
        List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
        for (ProcessorSlot slot : sortedSlotList) {
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }
            //将加载到的这些是AbstractLinkedProcessorSlot类型的插槽添加到这个调用链的末尾去
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }

在entryWithPriority方法中,最终会通过调用chain.entry来开启chain的责任链,开始执行chain中的entry方法,并且在entry的末尾又会调用fireEntry去调用注册了这个chain的下一个chain的entry方法。
而每一个slot它包含有first和next指针,以及相关的赋值和调用方法,可以用于这个调用链的连接和调用。

    @Override
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        //如果他的下一个slot不为空,就执行下一个的entry方法
        if (next != null) {
            next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
        }
    }

    void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
        throws Throwable {
        T t = (T)o;
        entry(context, resourceWrapper, t, count, prioritized, args);
    }

而这正就对应了上图中所示的调用插槽链的过程了,就会一个一个的把链中的所有插槽都执行完,到这里我们就能看到了,如上图所示的sentinel的运行流程的大致框架了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值