Sentinel 源码分析入门【Entry、Chain、Context】
前言:
前面我们分析了 Sentinel 的各种核心概念点以及 Sentinel 的执行流程,并分别演示了使用 Sentinel 编码和注解方式来管理资源的场景,并分别演示了使用 Sentinel 编码和注解方式来管理资源的场景,以及使用 Spring Cloud 集成 Nacos、Sentinel、OpenFeign 实现服务熔断降级的案例演示,本篇我们将从源码角度来分析 Sentinel 的原理。
Sentinel 系列文章传送门:
Spring Cloud 整合 Nacos、Sentinel、OpenFigen 实战【微服务熔断降级实战】
Entry 概念回忆
Entry 在 Sentinel 中代表资源,每一次资源调用都会创建一个 Entry,Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息,我们前面在演示资源被 Sentinel 中保护的案例中,编码方式中就有一个 Entry 关键字,而使用注解方式的时候,我们在 SentinelResourceAspect 切面类中也看到了Entry 关键字,Sentinel 源码分析我们将从 Entry 入手。
Sentinel 中的资源用 Entry 来表示,声明 Entry 的 API 模板:
//Sentinel 中的资源用 Entry 来表示,声明 Entry 的 API 模板:
private void sentinelTemplate() {
//资源名 比如方法名、接口名或其它可唯一标识的字符串
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
}
}
ConfigCacheService#dumpBeta 方法源码解析
我们从 SphU.entry(“resourceName”) 这行代码进入源码,发现这个方法并没有太多的逻辑,只将资源进行包装,最后调用了 CtSph#entryWithPriority 方法。
//SphU#entry 方法经过一些列的重载方法调用,最终调用 CtSph#entryWithPriority 方法。
//com.alibaba.csp.sentinel.SphU#entry(java.lang.String)
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
//com.alibaba.csp.sentinel.CtSph#entry(java.lang.String, com.alibaba.csp.sentinel.EntryType, int, java.lang.Object...)
@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
//将资源名称 资源类型 资源数量 等包装成一个 Resource 对象
StringResourceWrapper resource = new StringResourceWrapper(name, type);
return entry(resource, count, args);
}
//com.alibaba.csp.sentinel.CtSph#entry(com.alibaba.csp.sentinel.slotchain.ResourceWrapper, int, java.lang.Object...)
public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
//调用 CtSph#entryWithPriority 方法
return entryWithPriority(resourceWrapper, count, false, args);
}
CtSph#entryWithPriority 方法源码解析
CtSph#entryWithPriority 方法已经到了 Sentinel 的核对方法了,该方法主要做了一下几件事:
- 获取上下文 Context ,对上下文数量进行判断,如果上下文的梳理超过了阀值,不会进行规则校验,直接返回。
- 判断上下文 Context 是否为空,如果为空则使用默认的上下文对象。
- 判断全局规则检查开关是否打开,如果没有打开,就直接返回,不做任何规则校验。
- 构造处理链 chain,并对处理链为空判断,如果为空,不做规则校验直接返回,这里使用了责任链模式,这个链路里面就是之前提到过的各种 slot,后面的源码也是围绕这里展开。
- 使用资源 ResourceWrapper、chain、context 创建 Entry 对象,执行各个 slot 的逻辑。
//com.alibaba.csp.sentinel.CtSph#entryWithPriority(com.alibaba.csp.sentinel.slotchain.ResourceWrapper, int, boolean, java.lang.Object...)
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
//获取 上下文 Context
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.
//表示上下文数量已经超过阀值 不会进行规则检查 返回空的 Context
return new CtEntry(resourceWrapper, null, context);
}
if (context == null) {
// Using default context.
//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.
*/
//chain 为空判断
if (chain == null) {
//chain 为空 意味着超过了 Constants.MAX_SLOT_CHAIN_SIZE 6000 的限制 直接返没有 chain 的 Entry 意味着不做任何规则检查
return new CtEntry(resourceWrapper, null, context);
}
//创建 entry
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
//执行 slot chain 重点关注
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;
}
执行 Sentinel 的各个 Slot 的时候,我们发现两个对象会传递下去,一个是 chain,一个是 context,下面我们对 chain 和 context 分别进行解释。
ProcessorSlotChain
ProcessorSlotChain Sentinel 的核心骨架,将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking),目前的设计是 one slot chain per resource(每个资源一个 ProcessorSlotChain),因为某些 slot 是 per resource 的(比如 NodeSelectorSlot)。后面的源码分析就是围绕图中红色圈出来的部分这个顺序去分析的,这个 Slot 的顺序也是 Sentinel 的工作顺序。
Context 概念
- Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。
- Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。
- Context 是通过 ThreadLocal 传递的,后续的Slot都可以通过Context拿到 DefaultNode 或者 ClusterNode,从而获取统计数据,完成规则判断。
- Context初始化的过程中,会创建 EntranceNode,contextName 就是 EntranceNode的名称。
Context 的初始化
我们知道 Context 会贯穿整个调用链路的上下文,既然如此,那 Context 是何时初始化的呢?
遇到初始化相关的问题,我们首相应该想到的就是 Sping Boot 的自动装配,我们去 Sentinel 的 META-INF/spring.factories 下看看是否有相关的类。
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.alibaba.cloud.sentinel.SentinelWebAutoConfiguration,\
com.alibaba.cloud.sentinel.SentinelWebFluxAutoConfiguration,\
com.alibaba.cloud.sentinel.endpoint.SentinelEndpointAutoConfiguration,\
com.alibaba.cloud.sentinel.custom.SentinelAutoConfiguration,\
com.alibaba.cloud.sentinel.feign.SentinelFeignAutoConfiguration
org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker=\
com.alibaba.cloud.sentinel.custom.SentinelCircuitBreakerConfiguration
我们知道 Controller 是默认的资源,这里刚好有一个跟 Web 相关的类 SentinelWebAutoConfiguration,我们先看这个类。
SentinelWebAutoConfiguration 类解析
我们看到 SentinelWebAutoConfiguration 实现了 WebMvcConfigurer,是 SpringMVC 的配置类,可以配置 HandlerInterceptor,我们重点关注 addInterceptors 方法,该方法中加入了 SentinelWebInterceptor 拦截器,我们继续跟踪 SentinelWebInterceptor 这个类。
package com.alibaba.cloud.sentinel;
import com.alibaba.cloud.sentinel.SentinelProperties.Filter;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.SentinelWebInterceptor;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.DefaultBlockExceptionHandler;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.UrlCleaner;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.config.SentinelWebMvcConfig;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnProperty(
name = {"spring.cloud.sentinel.enabled"},
matchIfMissing = true
)
@ConditionalOnClass({SentinelWebInterceptor.class})
@EnableConfigurationProperties({SentinelProperties.class})
public class SentinelWebAutoConfiguration implements WebMvcConfigurer {
private static final Logger log = LoggerFactory.getLogger(SentinelWebAutoConfiguration.class);
@Autowired
private SentinelProperties properties;
@Autowired
private Optional<UrlCleaner> urlCleanerOptional;
@Autowired
private Optional<BlockExceptionHandler> blockExceptionHandlerOptional;
@Autowired
private Optional<RequestOriginParser> requestOriginParserOptional;
@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());
log.info("[Sentinel Starter] register SentinelWebInterceptor with urlPatterns: {}.", filterConfig.getUrlPatterns());
}
}
}
SentinelWebInterceptor 类源码解析
SentinelWebInterceptor 类实现了 AbstractSentinelInterceptor 类,纵观整个 SentinelWebInterceptor 类,没有看到和 Context 相关的方法,我们再看看 AbstractSentinelInterceptor 类。
public class SentinelWebInterceptor extends AbstractSentinelInterceptor {
private final SentinelWebMvcConfig config;
public SentinelWebInterceptor() {
this(new SentinelWebMvcConfig());
}
public SentinelWebInterceptor(SentinelWebMvcConfig config) {
super(config);
if (config == null) {
this.config = new SentinelWebMvcConfig();
} else {
this.config = config;
}
}
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;
}
}
protected String getContextName(HttpServletRequest request) {
return this.config.isWebContextUnify() ? super.getContextName(request) : this.getResourceName(request);
}
}
AbstractSentinelInterceptor 类源码解析
HandlerInterceptor 拦截器会拦截一切进入 Controller 的方法,执行 preHandle 前置拦截方法,而 Context 的初始化就是在这里完成的。
//com.alibaba.csp.sentinel.adapter.spring.webmvc.AbstractSentinelInterceptor#preHandle
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
try {
//获取资源名称 一般就是方法的请求路径
String resourceName = this.getResourceName(request);
//请求资源为空判断
if (StringUtil.isEmpty(resourceName)) {
//为空返回 true
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
String contextName = this.getContextName(request);
//创建 context
ContextUtil.enter(contextName, origin);
//创建 Entry 类型是 IN (这里的方法我们在上面的代码中有见到过这样的代码)
Entry entry = SphU.entry(resourceName, 1, EntryType.IN);
//Entery 设置到 request 中
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;
}
}
ContextUtil#enter 方法源码解析
ContextUtil#enter 方法是真正的创建 Context 的方法,该方法先会判断是否是默认的 ContextName,如果是默认的 ContextName 就抛出异常,否则调用 ContextUtil#trueEnter 创建 Context,该方法会经过一系列的判断,最终返回一个 Context,这里返回的 Context 可能是 NullContext。
//com.alibaba.csp.sentinel.context.ContextUtil#enter(java.lang.String, java.lang.String)
public static Context enter(String name, String origin) {
//是否是默认的 contextName
if ("sentinel_default_context".equals(name)) {
//不允许创建默认的 contextName
throw new ContextNameDefineException("The sentinel_default_context can't be permit to defined!");
} else {
return trueEnter(name, origin);
}
}
//com.alibaba.csp.sentinel.context.ContextUtil#trueEnter
protected static Context trueEnter(String name, String origin) {
//尝试从 contextHolder 获取 context contextHolder 是 ThreadLocal
Context context = (Context)contextHolder.get();
//context 为空
if (context == null) {
//context 为空
//本地缓存 DefaultNode Map
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
//从缓存中获取 DefaultNode
DefaultNode node = (DefaultNode)localCacheNameMap.get(name);
//DefaultNode 入口节点为空判断
if (node == null) {
//DefaultNode 为空
if (localCacheNameMap.size() > 2000) {
//如果 localCacheNameMap 大于 2000 设置 contextHolder 为空
setNullContext();
//返回 NullContext
return NULL_CONTEXT;
}
//加锁
LOCK.lock();
try {
//再次根据 contextName 从 contextNameNodeMap 获取 node 对象
node = (DefaultNode)contextNameNodeMap.get(name);
if (node == null) {
//node 为空
if (contextNameNodeMap.size() > 2000) {
//如果 localCacheNameMap 大于 2000 设置 contextHolder 为空
setNullContext();
//返回 NullContext
Context var9 = NULL_CONTEXT;
return var9;
}
//入口节点为空,初始化入口节点 EntranceNode
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), (ClusterNode)null);
// 添加入口节点到 ROOT
Constants.ROOT.addChild((Node)node);
//入口节点添加到缓存中
Map<String, DefaultNode> newMap = new HashMap(contextNameNodeMap.size() + 1);
//加入缓存
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
//重新复制
contextNameNodeMap = newMap;
}
} finally {
//解锁
LOCK.unlock();
}
}
//创建 context
context = new Context((DefaultNode)node, name);
//设置来源节点
context.setOrigin(origin);
//添加到 ThreadLocal 缓存中
contextHolder.set(context);
}
//返回 context
return context;
}
至此,Context 的初始化源码分析完毕。
如有不正确的地方请各位指出纠正。
更多推荐
所有评论(0)