sentinel源码解读

142 阅读6分钟

本文对sentinel的应用只是简单的介绍,主要篇幅是在对源码进行解读。

一、sentinel中的一些概念

资源

资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。我们可以简单的理解为被保护的业务就是资源。

规则

围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。

二、应用

本文基于spring环境,使用注解的方式对sentinel的使用进行说明。
基于注解的方式,需要对spring的aop有基础的认知。

依赖

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.4</version>
</dependency>

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.4</version>
</dependency>

注入bean

@Bean
public SentinelResourceAspect sentinelResourceAspect() {
    return new SentinelResourceAspect();
}

业务方法

下面的代码中带有@SentinelResource注解的方法就是一个资源。而loadRules就是在加载我们自定义的规则。

private void flowRule() { 
    FlowRule rule = new FlowRule(); 
    rule.setResource("getUser");
    rule.setCount(1);
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS); 
    
    List<FlowRule> rules = new ArrayList<>();
    rules.add(rule);
    FlowRuleManager.loadRules(rules);    
}

@SentinelResource(value = "getUser",blockHandler = "blockHandler",fallback = "fallBack")
private String getUser(@RequestParam Integer id){
    return get(id); 
}

三、源码解读

注解@SentinelResource

上面我们在进行限流保护的时候使用了@SentinelResource注解,这个注解上面有很多字段,我们挑重要的看一下。

  1. value:资源名称,具有唯一性,后续对资源的保护都是围绕资源名称开展的。
  2. blockHandler:当触发sentinel的BlockException后执行的操作,这个方法需要和资源类定义在同一个类。或者写在一个下面配置的类里面,但是必须是静态方法。
  3. blockHandlerClass:和第2条配合使用。
  4. exceptionsToIgnore:定义一些异常,捕获到这些异常需要向上抛出。这里的Ignore并不是让业务忽略,而是对sentinel来说是忽略。
  5. exceptionsToTrace:需要trace的异常
  6. fallback:当程序抛出了BlockException以外的异常,不属于exceptionsToIgnore,但属于exceptionsToTrace的异常,会执行fallBack方法。需要和资源在同一个类。
  7. fallbackClass:同样的上面的方法如果不和资源在同一个类可以放到 fallbackClass里面。同上3。

切面类SentinelResourceAspect

刚才在应用场景中我们注入了一个切面类:SentinelResourceAspect。所以sentinel的一系列功能肯定是在这个切面类里面实现的,通过这个类我们可以得到以下几个结论:

  1. 切面类里面有一个切入点表达式及一个环绕通知方法。
  2. 带有@SentinelResource注解的方法都是切入点。
  3. SphU.entry执行成功后必然会执行业务方法,也就说明当业务如果想被限流或熔断时肯定是entry抛出了异常进入了catch逻辑。
  4. BlockException异常执行handleBlockException
  5. 在exceptionsToIgnore里面定义的异常,向上抛出
  6. 在exceptionsToTrace里面的异常执行 handleFallback
  7. 以上异常处理逻辑注意优先级
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

    @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 {
            //执行sentinel逻辑
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            //执行业务方法
            return pjp.proceed();
        } catch (BlockException ex) {
            //执行注解上面定义的 blockHandler
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            //exceptionsToIgnore 定义在注解中
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            //exceptionsToTrace 定义在注解中
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                //执行注解上面定义的 fallback
                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());
            }
        }
    }
}

下面我们看一下sentinel的主要逻辑方法:entry = SphU.entry(xxx); 最终会调到这里,对资源进行了包装,生成resourceWrapper,然后调用entryWithPriority。

public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                           Object[] args) throws BlockException {
    StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
    return entryWithPriority(resource, count, prioritized, args);
}

接着执行下面的逻辑:这段逻辑里有效方法只有两个。

  1. 执行各种检查。
  2. 执行lookProcessChain。
  3. 执行chain.entry。
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.
        return new CtEntry(resourceWrapper, null, context);
    }

    if (context == null) {
        // Using default context.
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    // Global switch is close, no rule checking will do.
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }

    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) {
        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;
}

lookProcessChain

先来看这个这个方法主要做了以下事:

  1. 构建<资源,chain>容器 Map<ResourceWrapper, ProcessorSlotChain>,每个资源第一次调用时通过newSlotChain加载,后续所有的资源都使用缓存。
  2. newSlotChain是我们主要关注的方法,这里暂且将这个方法当成黑盒,先梳理主线逻辑,通过debug可以看到我们拿到的chain是一个链表结构的对象,由于链表不好截图描述,这里截图为生成链表前一步拿到的有序集合。如下图:
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    //从缓存中取资源对应的chain,这里是八股文中的 synchronized doble check。
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry size limit.可以看到这里对资源数量有限制,最大6k。
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }
                //这里是从文件真正加载的流程。
                chain = SlotChainProvider.newSlotChain();
                //这里为什么用一个新容器来存放,不是很理解。
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                    chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

用于生成链表结构的有序集合:

image.png

链表上这些类的继承关系:

image.png

chain.entry()

最重要的方法登场了:流程图如下:

  1. chain.entry首先调用头节点的entry方法,跳转到抽象类执行fireEntry。
  2. 抽象类的fireEntry执行next节点的entry方法。
  3. 然后跳转到next节点(实现类)的entry方法,执行sentinel逻辑。
  4. 执行结束后继续执行抽象类的fireEntry节点,直到next为空。
  5. 唯一有区别的是Statistics节点,它的sentinel逻辑是所有节点执行结束才执行。
  6. 可以看到,链表上每个slot节点执行不同的功能,如果想扩展功能,只需要加一个新的slot即可。
  7. 出于阅读体验,这些Slot上的逻辑本文不进行进一步解释,单开文章进行详解。

image.png

SlotChainProvider.newSlotChain()

下面说一说上面遗留的黑盒逻辑:构造链表的过程:

  1. SpiLoader.of(SlotChainBuilder.class):生成一个SpiLoader对象,全局缓存。
  2. 生成的SpiLoader进行loadFirstInstanceOrDefault,这里其实加载了一个SlotChainBuilder类型的实例。
  3. load的逻辑过程,简化为两步,其实就是从指定目录找到类名,有了className,反射一下就可以拿到实例对象了。
    String fullFileName = "META-INF/services/" + service.getName();
    urls = classLoader.getResources(fullFileName);
  4. 构建slotChainBuilder后,执行build方法。后面讲build方法。
public static ProcessorSlotChain newSlotChain() {
    if (slotChainBuilder != null) {
        return slotChainBuilder.build();
    }

    // Resolve the slot chain builder SPI.
    slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();

    if (slotChainBuilder == null) {
        // Should not go through here.
        RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
        slotChainBuilder = new DefaultSlotChainBuilder();
    } else {
        RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
            slotChainBuilder.getClass().getCanonicalName());
    }
    return slotChainBuilder.build();
}

看一下上面的第3步,再看下图,sentinel-core包下面META-INF/services下面是不是正好有我们希望看到的文件。这里面是一个Builder对象,拿到Builder对象后就可以build了。 image.png

Builder.build

  1. 和上面加载SlotChainBuilder的过程类似,这里是加载ProcessorSlot,区别在于SpiLoader.of中传的接口名不一样。
  2. 下图可以看到 META-INF/services/ + ProcessorSlot.fullClassName 的文件内容,发现有8个Slot。
  3. loadInstanceListSorted 就是加载并排序,在每个Slot上面都标有@Order,加载后根据Order进行排序,生成有序列表。这里的过程很像Spring中对PostProcessor的处理。
  4. 通过这种方式,我们可以很方便的对sentinel进行扩展。
public ProcessorSlotChain build() {
    ProcessorSlotChain chain = new DefaultProcessorSlotChain();

    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;
        }

        chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
    }

    return chain;
}

image.png

自定义Slot

  1. 我们写一个输出I Love You的Slot,代码如下,在FireEntry前后输出I Love You。
package com.recommend.slot;

import com.alibaba.csp.sentinel.context.Context;
import com.alibaba.csp.sentinel.node.DefaultNode;
import com.alibaba.csp.sentinel.slotchain.AbstractLinkedProcessorSlot;
import com.alibaba.csp.sentinel.slotchain.ResourceWrapper;
import com.alibaba.csp.sentinel.spi.Spi;

/**
 * @author author
 */
@Spi(order = -6500)
public class PrintLoveSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
            throws Throwable {
        System.out.println("I Love You,start");
        fireEntry(context, resourceWrapper, obj, count, prioritized, args);
        System.out.println("I Love You,end");
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}
  1. 在项目跟目录下创建以下文件,并在文件中写入上面定义的PrintLoveSlot的全限定类名。
    filePath:classPath/META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot
    file内容:com.recommend.slot.PrintLoveSlot image.png
  2. 打个断点看一下:是不是多了一个PrintLoveSlot image.png
  3. 之后每次请求受保护的资源都会打印I Love You。 image.png
  4. 眼尖的同学已经看到上面第3步的图片中比之前的截图不仅多了一个PrintLoveSlot,还多了一个ParamFlowSlot,没错,这是因为我在项目中引入了一个热点参数限流的依赖。这个依赖中就和我们自定义Slot的方式一样,在/META-INF/services下面加了一些料,如下图: image.png
  5. 热点参数限流maven依赖:
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-parameter-flow-control</artifactId>
    <version>1.8.4</version>
</dependency>

本文到此结束!!!有问题欢迎私信。