Sentinel的工作原理:https://github.com/alibaba/Sentinel/wiki
-
Sentinel会为所有的资源,以资源名为区分,创建各自的DefaultProcessorSlotChain,放在缓存中;
-
DefaultProcessorSlotChain的9个ProcessorSlot插槽都是通过SPI机制从 META/services/ 目录下加载的;
-
每一个ProcessorSlot 其实是一个 AbstractLinkedProcessorSlot 抽象链表处理器插槽,
有一个next属性,指向下一个Slot,当某一个Slot执行完后,会调用fireEntry()方法,
将请求转到下一个Slot继续执行。
-
最终完成责任链上所有ProcessorSlot的逻辑!
-
——————————————————————————————————————————————
-
Context链路上下文为request请求级别的,放在ThreadLocal中,请求结束即释放;
-
entranceNode是应用级别的,创建完成后,会缓存起来(key为contextName),下一个请求可以继续使用;
-
processorSlotChain也是应用级别的,创建完成后,会缓存起来(key为resourceName),下一个请求可以继续使用;
一、ProcessorSlotChain处理器插槽链和Node节点的引入
首先,根据官方Wiki中的图,我们可以很形象地看到,整个请求处理过程就像一个链条一样,一步步地向后执行,这是一种典型地“责任链模式”;
责任链模式 —— 为请求创建一个接收者对象的链,链上的每一个节点服务处理各自的业务逻辑,实现解耦,每一个处理者节点记录着下一个节点的引用,请求将沿着这条链被传递下去,以此处理对应的逻辑。
1、ProcessorSlotChain处理器插槽链的引入
ProcessorSlotChain是上图整个链的骨架,基于“责任链模式”设计,将“统计、授权、限流、降级等”处理逻辑封装成一个个的Slot插槽,串联起来。
处理链中的Slot插槽可粗分为上下两大类:数据统计部分 + 规则判断部分
-
数据统计:
-
NodeSelectorSlot:负责构建簇点链路中的各个节点(DefaultNode),形成NodeTree
-
ClusterBuilderSlot:负责构建某个资源的ClusterNode(具体的DefaultNode和ClusterNode的区别见下文)
-
StatisticSlot:负责实时统计请求的各种调用信息,如来源信息、请求次数、运行信息等;
-
规则判断:
-
AuthoritySlot:授权规则判断(来源控制)
-
SystemSlot:系统保护规则判断,当系统资源使用量达到一定程度后,拒绝新的请求进入等;
-
ParamFlowSlot:热点参数限流规则判断
-
FlowSolt:普通限流规则判断
-
DegradeSlot:降级规则判断
2、为什么要存在NodeSelectorSlot和ClusterBuilderSlot两个插槽?DefaultNode和ClusterNode有什么区别?

-
DefaultNode:同一份资源,经过不同的链路调用,会创建不同的DefaultNode,记录不同链路访问当前资源的统计元数据,因为整个Sentinel是支持“根据链路限流”的,所以肯定要分开统计;
-
ClusterNode:同一份资源,在整个系统中只会创建一个ClusterNode,记录所有入口访问当前资源的统计元数据,因为很多时候,我们只需要统计该资源的整体使用情况。
注意这里的用词,DefaultNode和ClusterNode都只是负责记录统计元数据,真正的统计工作由之后的StatisticSlot进行,另外ParmFlowSlot会负责热点参数限流这种特殊场景下的数据统计。(热点参数限流的统计为什么要单独出来,后面做限流算法实现的讲解时就清楚了)。
3、如何自定义一个Sentinel资源?@SentinelResource注解?
我们知道,在实际使用过程中,当我们要自定义sentinel资源时,只需要使用@SentinelResource注解定义即可,很方便。
而且Sentinel默认就已经将 springmvc 的 controller 中的方法注册为sentinel资源了,但是这些方法并没有添加 @SentinelResource 注解呀!
其实@SentinelResource底层也就是通过AOP + Entry 的方式来手动注册 Sentinel资源的:
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
SentinelResourceAspect切面类:
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
@Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
public void sentinelResourceAnnotationPointcut() {
}
// 经典AOP实现
@Around("sentinelResourceAnnotationPointcut()")
public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
Method originMethod = this.resolveMethod(pjp);
SentinelResource annotation = (SentinelResource)originMethod.getAnnotation(SentinelResource.class);
if (annotation == null) {
throw new IllegalStateException("Wrong state for SentinelResource annotation");
} else {
String resourceName = this.getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
int resourceType = annotation.resourceType();
Entry entry = null;
try {
Object var18;
try {
// 注册对应的资源
entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
// 执行具体的业务逻辑
return pjp.proceed();
} catch (Exception e) {
......
}
} finally {
......
}
}
}
}
所以,通过Entry手动注册资源 和 通过@SentinelResource 注解自动注入资源,原理上时一样的,都是通过 SphU.entry(…) 方法实现。
4、链路上下文Context
public class Context {
private final String name;
private DefaultNode entranceNode;
private Entry curEntry;
private String origin = "";
private final boolean async;
}
-
Context代表调用链路的上下文,贯穿一次链路调用中的所有资源(Entry),基于ThreadLocal实现;
-
Context维护者入口节点(entranceNode)、当前资源节点(curEntry —>curNode)、调用来源origin等信息;
-
后续所有的Slot插槽都可以通过context拿到DefaultNode 和 ClusterNode,从而完成统计或判断逻辑;
-
Context创建过程中,会创建EntranceNode,contextName 就是entranceNode的名称;
// 创建context,包含两个参数:context名称、 来源名称
ContextUtil.enter("contextName", "originName");
-
默认情况下,Sentinel的entranceNode是sentinel_default_context,如果我们要想做链路限流,就必须关闭“统一入口配置”,从而让每一个Controller方法为Context的入口。
public final static String CONTEXT_DEFAULT_NAME = "sentinel_default_context";
spring: cloud: sentinel: web-context-unify: false # 关闭context统一入口配置
二、Sentinel源码剖析——Context的初始化
1、spring-cloud-starter-alibaba-sentinel 的 spring.factory 中有两个相关的自动装配类:

由于,Context的初始化,涉及到了将Controller中的方法定义为entranceNode的过程,所以肯定是看 SentinelWebAutoConfiguration 这个自动装配类!
2、向 springmvc 处理链中添加一个Sentinel的拦截器:
@Configuration(
proxyBeanMethods = false // Lite模式,关闭Full模式
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnProperty(
name = {"spring.cloud.sentinel.enabled"},
matchIfMissing = true
)
@ConditionalOnClass({SentinelWebInterceptor.class})
@EnableConfigurationProperties({SentinelProperties.class})
public class SentinelWebAutoConfiguration implements WebMvcConfigurer {
// 通过实现 WebMvcConfigurer 接口,允许了手动向springmvc中添加拦截器 Interceptor
......
// 注入本类下文定义的SentinelWebInterceptor
@Autowired
private Optional<SentinelWebInterceptor> sentinelWebInterceptorOptional;
public SentinelWebAutoConfiguration() {
}
// 添加
public void addInterceptors(InterceptorRegistry registry) {
if (this.sentinelWebInterceptorOptional.isPresent()) {
Filter filterConfig = this.properties.getFilter();
registry.addInterceptor((HandlerInterceptor)this.sentinelWebInterceptorOptional.get())
.order(filterConfig.getOrder()).addPathPatterns(filterConfig.getUrlPatterns());
}
}
// 向 IOC 容器中注入一个 SentinelWebInterceptor 拦截器
@Bean
@ConditionalOnProperty(
name = {"spring.cloud.sentinel.filter.enabled"},
matchIfMissing = true
)
public SentinelWebInterceptor sentinelWebInterceptor(SentinelWebMvcConfig sentinelWebMvcConfig) {
return new SentinelWebInterceptor(sentinelWebMvcConfig);
}
}
3、SentinelWebInterceptor拦截器的核心方法:
SentinelWebInterceptor 中会对父类 AbstractSentinelInterceptor 中的抽象方法做实现(模板方法模式):
public class SentinelWebInterceptor extends AbstractSentinelInterceptor {
// 获取resourceName:
// controller中请求方法的路径(资源):/order/{orderId}
protected String getResourceName(HttpServletRequest request) {
Object resourceNameObject = request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (resourceNameObject != null && resourceNameObject instanceof String) {
String resourceName = (String)resourceNameObject;
UrlCleaner urlCleaner = this.config.getUrlCleaner();
if (urlCleaner != null) {
resourceName = urlCleaner.clean(resourceName);
}
if (StringUtil.isNotEmpty(resourceName) && this.config.isHttpMethodSpecify()) {
resourceName = request.getMethod().toUpperCase() + ":" + resourceName;
}
return resourceName;
} else {
return null;
}
}
// 获取contextName:
// 如果开启了统一入口配置,则contextName就是默认的统一入口:sentinel_spring_web_context
// 如果关闭了统一入口配置,则contextName就是当前资源的名称;
protected String getContextName(HttpServletRequest request) {
return this.config.isWebContextUnify() ? super.getContextName(request) : this.getResourceName(request);
}
}
而作为一个拦截器,最重要的逻辑,肯定是在 prehandler() 中:
public abstract class AbstractSentinelInterceptor implements HandlerInterceptor {
public static final String SENTINEL_SPRING_WEB_CONTEXT_NAME = "sentinel_spring_web_context";
private static final String EMPTY_ORIGIN = "";
private final BaseWebMvcConfig baseWebMvcConfig;
// 前置拦截的核心逻辑
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
// 获取资源名称,一般是controller方法的@RequestMapping路径,例如/order/{orderId}
String resourceName = this.getResourceName(request);
if (StringUtil.isEmpty(resourceName)) {
return true;
} else if (this.increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), 1) != 1) {
return true;
} else {
// 从request中获取请求来源,将来做 授权规则(来源控制) 判断时会用
String origin = this.parseOrigin(request);
// 获取 contextName,默认是sentinel_spring_web_context;
// 如果关闭统一入口,那就是当前resourceName
String contextName = this.getContextName(request);
// 创建Context核心方法
ContextUtil.enter(contextName, origin);
// 构建ProcessorSlotChain处理器插槽链的核心逻辑
Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
request.setAttribute(this.baseWebMvcConfig.getRequestAttributeName(), entry);
return true;
}
} catch (BlockException var12) {
BlockException e = var12;
try {
this.handleBlockException(request, response, e);
} finally {
ContextUtil.exit();
}
return false;
}
}
// 当请求体业务处理完成后,关闭所有的资源
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (this.increaseReferece(request, this.baseWebMvcConfig.getRequestRefName(), -1) == 0) {
Entry entry = this.getEntryInRequest(request, this.baseWebMvcConfig.getRequestAttributeName());
if (entry == null) {
...Log...
} else {
this.traceExceptionAndExit(entry, ex); // entry.exit()退出
this.removeEntryInRequest(request);
ContextUtil.exit(); // contextHolder.set(null);
}
}
}
}
// private static ThreadLocal<Context> contextHolder = new ThreadLocal<>(); // 保存context的threadLocal
4、ContextUtil.enter(contextName, origin) 创建Context核心方法:
// com.alibaba.csp.sentinel.context.ContextUtil#enter
public static Context enter(String name, String origin) {
// "sentinel_default_context"是不允许被创建的
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}
|
|
// com.alibaba.csp.sentinel.context.ContextUtil#trueEnter
protected static Context trueEnter(String name, String origin) {
// 尝试获取context,一般一个新的请求到达后,获取context肯定为null
Context context = contextHolder.get();
// 判空
if (context == null) {
// 如果为空,开始初始化
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
// 尝试获取入口节点
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) { // 双重检测锁
// 入口节点为空,初始化入口节点 EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// 添加入口节点到 ROOT,所有的节点共用一个ROOT根节点
Constants.ROOT.addChild(node);
// 将入口节点放入缓存(下次请求时候,根据contextName获取,可直接使用)
Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap; // CopyOnWrite
}
} finally {
LOCK.unlock();
}
}
// 创建Context,参数为:入口节点 和 contextName
context = new Context(node, name);
// 设置请求来源 origin
context.setOrigin(origin);
// 将context放入ThreadLocal
contextHolder.set(context);
}
// 返回
return context;
}
由此我们可以得出重要结论:
在每一个请求到达时,Sentinel的拦截器都会为本次请求封装一个“链路上下文context”,然后放入到ThreadLocal中,便于请求在后面的处理过程中取用;
默认情况下,“统一入口配置开启”,“链路上下文context”以sentinel-spring-web-context 命名;
如果关闭了“统一入口配置”,“链路上下文context”将以本次请求对应的controller方法的 @RequestMapping() 的值命名,如“/order/{orderId}”;
由于context是放在Thread中的,所以当本次请求结束后,context就会被释放,下次请求需要重新创建;(context生命周期为request)
但是入口 entranceNode 却是放在缓存HashMap中的,所以下一次新的请求到达时,就没有必要再重新创建了;(entranceNode生命周期为应用级)
创建入口方法entranceNode时,使用了双重检测锁 + CopyOnWrite,因为存在多个请求线程并发情况;
创建context过程不需要考虑多线程安全,原因也是因为context时线程内的,单线程。
三、Sentinel核心源码之ProcessorSlotChain的构建
1、入口方法,正式上文拦截器中创建Context之后的方法:
Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
该方法,将“一脉单传”调用到以下方法 entryWithPriority() :
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 获取 Context
Context context = ContextUtil.getContext();
if (context == null) {
// Using default context.
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// 获取 Slot执行链,同一个资源(如:/order/{orderId}),会创建一个执行链,放入缓存
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
// 创建 Entry,并将 resource、chain、context 记录在 Entry中
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
// 执行 slotChain
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
// 如果执行 slotChain 过程中发生异常,也直接将对应的资源释放
e.exit(count, args);
......
}
return e;
}
2、lookProcessChain() 创建或获取资源对应的ProcessorSlotChain的方法:
// com.alibaba.csp.sentinel.CtSph#lookProcessChain
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
// 从缓存chainMap中获取
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) { // 又是双重检测锁
// Entry size limit.
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}
// 入口本资源对应的chain不存在,则创建一个新的
chain = SlotChainProvider.newSlotChain();
Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
chainMap.size() + 1);
newMap.putAll(chainMap);
newMap.put(resourceWrapper, chain);
chainMap = newMap; // 又是CopyOnWrite
}
}
}
return chain;
}
虽然每一次请求的ResourceWrapper都是新new的,但是由于它的hashCode() 和 equals() 方法,只会对比 name;
public abstract class ResourceWrapper {
protected final String name;
protected final EntryType entryType;
protected final int resourceType;
@Override
public int hashCode() {
return getName().hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof ResourceWrapper) {
ResourceWrapper rw = (ResourceWrapper)obj;
return rw.getName().equals(getName());
}
return false;
}
}
所以,得出结论:
Sentinel会为所有的资源,以资源名为区分,创建对应的ProcessorSlotChain,并缓存到chainMap中;
ProcessorSlotChain应用级有效,创建后,下次相同名称的Resource请求进入时,将不需要再次创建chain;
3、SlotChainProvider.newSlotChain() 处理器插槽链的构建过程:
// com.alibaba.csp.sentinel.slotchain.SlotChainProvider#newSlotChain
public static ProcessorSlotChain newSlotChain() {
if (slotChainBuilder != null) {
return slotChainBuilder.build();
}
// 默认肯定是得到一个 DefaultSlotChainBuilder
slotChainBuilder = SpiLoader.loadFirstInstanceOrDefault(SlotChainBuilder.class, DefaultSlotChainBuilder.class);
if (slotChainBuilder == null) {
slotChainBuilder = new DefaultSlotChainBuilder();
} else {
......
}
return slotChainBuilder.build();
}
|
|
public class DefaultSlotChainBuilder implements SlotChainBuilder {
@Override
public ProcessorSlotChain build() {
// 创建一个 DefaultProcessorSlotChain
ProcessorSlotChain chain = new DefaultProcessorSlotChain();
// 该方法会通过spi机制从 \META-INF\services\目录下,加载所有的ProcessorSlot类
List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
for (ProcessorSlot slot : sortedSlotList) {
if (!(slot instanceof AbstractLinkedProcessorSlot)) {
continue;
}
// 最终创建的
chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
}
return chain;
}
}
4、SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class)通过SPI机制加载所有的ProcessorSlot插槽类:
// com.alibaba.csp.sentinel.util.SpiLoader#loadPrototypeInstanceListSorted
public static <T> List<T> loadPrototypeInstanceListSorted(Class<T> clazz) {
try {
// Not use SERVICE_LOADER_MAP, to make sure the instances loaded are different.
ServiceLoader<T> serviceLoader = ServiceLoaderUtil.getServiceLoader(clazz);
List<SpiOrderWrapper<T>> orderWrappers = new ArrayList<>();
// SPI机制会从本地的 META-INF/services/ 目录下加载 ProcessorSlot 列表;
for (T spi : serviceLoader) {
int order = SpiOrderResolver.resolveOrder(spi);
// Since SPI is lazy initialized in ServiceLoader, we use online sort algorithm here.
SpiOrderResolver.insertSorted(orderWrappers, spi, order);
}
List<T> list = new ArrayList<>(orderWrappers.size());
for (int i = 0; i < orderWrappers.size(); i++) {
list.add(orderWrappers.get(i).spi);
}
return list;
} catch (Throwable t) {
t.printStackTrace();
return new ArrayList<>();
}
}
本地 META/services/ 目录下的 ProcessorSlot文件定义了9个插槽!

5、最终构建成的ProcessorSlotChain的结构:
首先,所有的9大ProcessorSlot都继承于一个AbstractLinkedProcessorSlot类:
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
private AbstractLinkedProcessorSlot<?> next = null;
// fireEntry的作用主要就是让请求流转到下一个ProcessorSlot(如果存在的话)
@Override
public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
if (next != null) {
next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
}
}
// 所有ProcessorSlot的入口方法,其中会通过模板方法模式,调用各自的entry处理逻辑
// 而再所有的处理逻辑的最后,都会再调一次 fireEntry() 方法
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);
}
}
经过 next 指向,最终构建出来的 DefaultProcessorSlotChain 如下:

综上总结:
Sentinel会为所有的资源,以资源名为区分,创建各自的DefaultProcessorSlotChain,放在缓存中;
DefaultProcessorSlotChain的每一个ProcessorSlot插槽都是通过SPI机制从 META/services/ 目录下加载的;
每一个ProcessorSlot 其实是一个 AbstractLinkedProcessorSlot 抽象链表处理器插槽,有一个next属性,指向下一个Slot,当某一个Slot执行完后,会调用fireEntry()方法,将请求转到下一个Slot继续执行。
最终完成责任链上所有ProcessorSlot的逻辑!
四、九大ProcessorSlot处理器插槽的工作原理
LogSlot插槽是一个边缘插槽,做一些日志记录,所以不算重要,排除在外后,就剩8大插槽,也就是<第一章节>列出的八大插槽:
数据统计部分 + 规则判断部分
-
数据统计:
-
NodeSelectorSlot:负责构建簇点链路中的各个节点(DefaultNode),形成NodeTree
-
ClusterBuilderSlot:负责构建某个资源的ClusterNode(具体的DefaultNode和ClusterNode的区别见下文)
-
StatisticSlot:负责实时统计请求的各种调用信息,如来源信息、请求次数、运行信息等;
-
规则判断:
-
AuthoritySlot:授权规则判断(来源控制)
-
SystemSlot:系统保护规则判断,当系统资源使用量达到一定程度后,拒绝新的请求进入等;
-
ParamFlowSlot:热点参数限流规则判断
-
FlowSolt:普通限流规则判断
-
DegradeSlot:降级规则判断
其实,当Sentinel的整体架构,和调用逻辑梳理清楚后,每一个责任链节点的处理逻辑就很简单了,所以,以后有机会再补充吧。
略!




