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用于增强原方法。
在该增强方法中所做的事情:
- 获取注解中的资源名,入口类型等信息
- 调用SphU.entry去获取一个token,所有的数据统计和规则检查都是在这个方法里面进行的
- 执行原资源的目标方法
- 如果一些规则不通过,处理异常,如执行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方法,做了下面这些事情:
- 尝试获取当前线程对应的上下文
- context数量已经超出阈值就返回不带规则检查的CtEntry
- 没有获取到上下文就创建一个默认的上下文
- 查询该资源需要调用的处理槽链
- slotchain数量超出阈值,也会返回不带规则检查的CtEntry
- 进入插槽调用链路,做规则检查等操作
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的运行流程的大致框架了。