Sentinel 源码分析入门【Entry、Chain、Context】

前言:

前面我们分析了 Sentinel 的各种核心概念点以及 Sentinel 的执行流程,并分别演示了使用 Sentinel 编码和注解方式来管理资源的场景,并分别演示了使用 Sentinel 编码和注解方式来管理资源的场景,以及使用 Spring Cloud 集成 Nacos、Sentinel、OpenFeign 实现服务熔断降级的案例演示,本篇我们将从源码角度来分析 Sentinel 的原理。

Sentinel 系列文章传送门:

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 的初始化源码分析完毕。

如有不正确的地方请各位指出纠正。

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值