前言:
上一篇文章中,我们对 Sentinel 有了基本认知,知道其是 Alibaba 开源的一个服务稳定性组件,我们从 Sentinel 控制台认识了 Sentinel 的流控、降级、热点、授权规则,本篇我们将从核心概念和工作流程方面继续分析 Sentinel。
Sentinel 系列文章传送门:
Sentinel 初步认识及使用
Sentinel 两个核心概念
Sentinel 实现限流、隔离、降级、熔断等功能,其本质就是做了两件事情,统计数据和规则判断。
- 统计数据:统计某个资源的访问数据,比如 QPS、RT、是否异常请求等。
- 规则判断:判断限流规则、隔离规则、降级规则、熔断规则是否满足。
资源就是指被 Sentinel 保护的业务,例如项目中定义的 controlleer 方法就是默认被 Sentinel 保护的资源。
ProcessorSlotChain
Sentinel 实现试数据统计和规则判断的核心类是 ProcessorSlotChain,这个类基于责任链模式来设计,将不同的功能
封装为一个个的 Slot,请求进入后逐个执行即可,执行流程图如下:
图摘自 Github,Sentinel Github wiki 地址
Slot
数据统计 Slot(statistic)
- NodeSelectorSlot:负责构建簇点链路中的节点(DefaultNode),将这些节点形成链路树。
- ClusterBuilderSlot:负责构建某个资源的 ClusterNode,ClusterNoode 可以保存资源的运行信息(响应时间、QPS、block数目、线程数、异常数等)以及来源信息(origin名称)。
- StatisticSlot:负责统计实时调用数据,包括运行信息、来源信息等。
规则判断 Slot(rulechecking)
- AuthoritySlot:负责授权规则(来源控制)。
- SystemSlot:负责系统保护规则。
- ParamFlowSlot:负责热点参数限流规则。
- FlowSlot:负责限流规则。
- DegradeSlot:负责降级规则。
Context
Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。Context 维持的方式:通过 ThreadLocal 传递,只有在入口 enter 的时候生效。由于 Context 是通过 ThreadLocal 传递的,因此对于异步调用链路,线程切换的时候会丢掉 Context,因此需要手动通过 ContextUtil.runOnContext(context, f) 来变换 context。
Entry
每一次资源调用都会创建一个 Entry。Entry 包含了资源名、curNode(当前统计节点)、originNode(来源统计节点)等信息。CtEntry 为普通的 Entry,在调用 SphU.entry(xxx) 的时候创建。特性:Linked entry within current context(内部维护着 parent 和 child)需要注意的一点:CtEntry 构造函数中会做调用链的变换,即将当前 Entry 接到传入 Context 的调用链路上(setUpEntryFor)。资源调用结束时需要 entry.exit()。exit 操作会过一遍 slot chain exit,恢复调用栈,exit context 然后清空 entry 中的 context 防止重复调用。
Node
Sentinel 里面的各种种类的统计节点:
- StatisticNode:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构。
- DefaultNode:链路节点,用于统计调用链路上某个资源的数据,维持树状结构。
- ClusterNode:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为 StatisticNode)。特别地,Constants.ENTRY_NODE 节点用于统计全局的入口资源数据。
- EntranceNode:入口节点,特殊的链路节点,对应某个 Context 入口的所有调用数据。Constants.ROOT 节点也是入口节点。
构建的时机:
- EntranceNode 在 ContextUtil.enter(xxx) 的时候就创建了,然后塞到 Context 里面。
- NodeSelectorSlot:根据 context 创建 DefaultNode,然后 set curNode to context。
- ClusterBuilderSlot:首先根据 resourceName 创建 ClusterNode,并且 set clusterNode to defaultNode;然后再根据 origin 创建来源节点(类型为 StatisticNode),并且 set originNode to curEntry。
几种 Node 的维度(数目):
- ClusterNode 的维度是 resource。
- DefaultNode 的维度是 resource * context,存在每个 NodeSelectorSlot 的 map 里面。
- EntranceNode 的维度是 context,存在 ContextUtil 类的 contextNameNodeMap 里面。
- 来源节点(类型为 StatisticNode)的维度是 resource * origin,存在每个 ClusterNode 的 originCountMap 里面。
StatisticSlot
StatisticSlot 是 Sentinel 最为重要的类之一,用于根据规则判断结果进行相应的统计操作。entry 的时候:依次执行后面的判断 slot。每个 slot 触发流控的话会抛出异常(BlockException 的子类)。若有 BlockException 抛出,则记录 block 数据;若无异常抛出则算作可通过(pass),记录 pass 数据。exit 的时候:若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数-1。记录数据的维度:线程数+1、记录当前 DefaultNode 数据、记录对应的 originNode 数据(若存在 origin)、累计 IN 统计数据(若流量类型为 IN)。
Sentinel 定义资源的两种方式
Sentinel 默认会将 Controller 中的方法作为被保护资源,我们来演示一下非 Controller 中方法被 Sentinel 保护的两种方式。
-
编码定义资源。
编码模板如下:
private void sentinelTemplate() {
//资源名 比如方法名、接口名或其它可唯一标识的字符串
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
}
}
编码定义资源演示
@GetMapping("/query-order-by-id")
public String queryOrderById(@RequestParam String orderId) {
return orderService.queryOrderById(orderId);
}
@Override
public String queryOrderById(String orderId) {
try (Entry ignored = SphU.entry("orderResouce")) {
return "订单号:" + orderId;
} catch (BlockException e) {
log.error("异常信息:", e);
return null;
}
}
请求接口,Sentinel 控制台如下:
2. @SentinelResource 注解定义资源。
注解定义资源演示:
@SentinelResource(value = "sentinelResource")
@Override
public String testSentinelResource(String sentinelResource) {
return "sentinelResource 测试:" + sentinelResource;
}
@GetMapping("/test-sentinel-resource")
public String testSentinelResource(@RequestParam String testSentinelResource) {
return orderService.testSentinelResource(testSentinelResource);
}
注解演示结果:
@SentinelResource 注解的原理
我们上面演示了使用编码方式和注解方式都可以把资源交给 Sentinel 保护,编码方式容易理解,那注解方式下资源是怎么被 Sentinel 保护的呢?我们大胆猜测是否是使用 AOP 切面的方式实现的呢?我们通过搜索发现还真有一个叫做 SentinelResourceAspect 的类,如下:
@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);
//获取 entry 类型
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) {
//异常处理
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());
}
}
}
}
通过 SentinelResourceAspect 类的源码可以知道 @SentinelResource 注解是一个标记,而 Sentinel 基于 AOP 思想,对被标记的方法做环绕增强,完成资源 Entry 的创建,实现了对资源的保护。
SentinelResourceAspect 类是何时加载的?
源码阅读的多了,不难想到去 Sentinel 的 META-INF/spring.properties 文件查看一下是否有相关类,spring.properties 如下:
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
从 spring.properties 不难看出有 SentinelAutoConfiguration 类可能会更 SentinelResourceAspect 类的加载有关系,我们来验证一下。
果然在 SentinelAutoConfiguration 类中发现了一段定义 SentinelResourceAspect 的方法,如下:
@Bean
@ConditionalOnMissingBean
public SentinelResourceAspect sentinelResourceAspect() {
return new SentinelResourceAspect();
}
至此 SentinelResourceAspect 类的加载时机已经很清楚了,SentinelAutoConfiguration 是 Spring Boot 自动装配的,而 SentinelAutoConfiguration 类中又定义了 SentinelResourceAspect 类。
欢迎提出建议及对错误的地方指出纠正。