本文对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注解,这个注解上面有很多字段,我们挑重要的看一下。
- value:资源名称,具有唯一性,后续对资源的保护都是围绕资源名称开展的。
- blockHandler:当触发sentinel的BlockException后执行的操作,这个方法需要和资源类定义在同一个类。或者写在一个下面配置的类里面,但是必须是静态方法。
- blockHandlerClass:和第2条配合使用。
- exceptionsToIgnore:定义一些异常,捕获到这些异常需要向上抛出。这里的Ignore并不是让业务忽略,而是对sentinel来说是忽略。
- exceptionsToTrace:需要trace的异常
- fallback:当程序抛出了BlockException以外的异常,不属于exceptionsToIgnore,但属于exceptionsToTrace的异常,会执行fallBack方法。需要和资源在同一个类。
- fallbackClass:同样的上面的方法如果不和资源在同一个类可以放到 fallbackClass里面。同上3。
切面类SentinelResourceAspect
刚才在应用场景中我们注入了一个切面类:SentinelResourceAspect。所以sentinel的一系列功能肯定是在这个切面类里面实现的,通过这个类我们可以得到以下几个结论:
- 切面类里面有一个切入点表达式及一个环绕通知方法。
- 带有@SentinelResource注解的方法都是切入点。
- SphU.entry执行成功后必然会执行业务方法,也就说明当业务如果想被限流或熔断时肯定是entry抛出了异常进入了catch逻辑。
- BlockException异常执行handleBlockException
- 在exceptionsToIgnore里面定义的异常,向上抛出
- 在exceptionsToTrace里面的异常执行 handleFallback
- 以上异常处理逻辑注意优先级
@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);
}
接着执行下面的逻辑:这段逻辑里有效方法只有两个。
- 执行各种检查。
- 执行lookProcessChain。
- 执行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
先来看这个这个方法主要做了以下事:
- 构建<资源,chain>容器 Map<ResourceWrapper, ProcessorSlotChain>,每个资源第一次调用时通过newSlotChain加载,后续所有的资源都使用缓存。
- 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;
}
用于生成链表结构的有序集合:
链表上这些类的继承关系:
chain.entry()
最重要的方法登场了:流程图如下:
- chain.entry首先调用头节点的entry方法,跳转到抽象类执行fireEntry。
- 抽象类的fireEntry执行next节点的entry方法。
- 然后跳转到next节点(实现类)的entry方法,执行sentinel逻辑。
- 执行结束后继续执行抽象类的fireEntry节点,直到next为空。
- 唯一有区别的是Statistics节点,它的sentinel逻辑是所有节点执行结束才执行。
- 可以看到,链表上每个slot节点执行不同的功能,如果想扩展功能,只需要加一个新的slot即可。
- 出于阅读体验,这些Slot上的逻辑本文不进行进一步解释,单开文章进行详解。
SlotChainProvider.newSlotChain()
下面说一说上面遗留的黑盒逻辑:构造链表的过程:
- SpiLoader.of(SlotChainBuilder.class):生成一个SpiLoader对象,全局缓存。
- 生成的SpiLoader进行loadFirstInstanceOrDefault,这里其实加载了一个SlotChainBuilder类型的实例。
- load的逻辑过程,简化为两步,其实就是从指定目录找到类名,有了className,反射一下就可以拿到实例对象了。
String fullFileName = "META-INF/services/" + service.getName();
urls = classLoader.getResources(fullFileName); - 构建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了。
Builder.build
- 和上面加载SlotChainBuilder的过程类似,这里是加载ProcessorSlot,区别在于SpiLoader.of中传的接口名不一样。
- 下图可以看到 META-INF/services/ + ProcessorSlot.fullClassName 的文件内容,发现有8个Slot。
- loadInstanceListSorted 就是加载并排序,在每个Slot上面都标有@Order,加载后根据Order进行排序,生成有序列表。这里的过程很像Spring中对PostProcessor的处理。
- 通过这种方式,我们可以很方便的对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;
}
自定义Slot
- 我们写一个输出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);
}
}
- 在项目跟目录下创建以下文件,并在文件中写入上面定义的PrintLoveSlot的全限定类名。
filePath:classPath/META-INF/services/com.alibaba.csp.sentinel.slotchain.ProcessorSlot
file内容:com.recommend.slot.PrintLoveSlot - 打个断点看一下:是不是多了一个PrintLoveSlot
- 之后每次请求受保护的资源都会打印I Love You。
- 眼尖的同学已经看到上面第3步的图片中比之前的截图不仅多了一个PrintLoveSlot,还多了一个ParamFlowSlot,没错,这是因为我在项目中引入了一个热点参数限流的依赖。这个依赖中就和我们自定义Slot的方式一样,在/META-INF/services下面加了一些料,如下图:
- 热点参数限流maven依赖:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>1.8.4</version>
</dependency>