- Resource:Sentinel中最核心的概念,其通过资源来保护具体的业务代码或其他后方服务,用户只需要将代码或者服务定义为一个资源,然后再定义规则就可以了,剩下的交给sentinel来处理。并且资源和规则时解耦的,规则可在运行是动态修改;定义完资源后,就可以通过在程序中埋点来保护你自己的服务了,埋点的方式有两种:
- try…catch的方式【通过 SphU.entry(...)】:当catch到BlockException时,执行异常处理或者fallback。
- if…else的方式【通过 SphO.entry(...)】:当返回false时执行异常处理或者fallback。其实内部是将ry…catch的方式抛出的异常BlockException进行包装,有异常返回false。
//SphO.entry(...)
public static boolean entry(String name, EntryType type, int count, Object... args) {
try {
Env.sph.entry(name, type, count, args);
} catch (BlockException e) {
return false;
} catch (Throwable e) {
RecordLog.info("[Sentinel] Fatal error", e);
return true;
}
return true;
}
//SphU.entry(...)
public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
return Env.sph.entry(name, type, count, args);
}
-
用户采用Sph…的方式定义资源对业务代码侵入很大,所以Sentinel提供了注解式【SentinelResource**】**来定义资源,并定义资源对应异常处理blockHandler。
-
Sentinel中资源对应的类为ResourceWrapper,其为抽象的包装类,包装了资源的 Name 和**EntryType,**具体有两个实现类,分别是:StringResourceWrapper 和 MethodResourceWrapper``。
- StringResourceWrapper:是对String对一串字符串进行包装,是一个通用的资源包装类。
- MethodResourceWrapper: 是对方法调用的包装。
Sentinel的工作流程是围绕着一个个插槽所组成的槽链来实现的。Sentinel默认的各个插槽之间的顺序是固定的,因为有的插槽实现需要依赖其他插槽的数据统计。Sentinel也提供用户自定义方式实现插槽,但则只能放在默认Slot链的尾部。如果用户想自定义Slot链,可以实现**SlotChainBuilder**来自定义Slot的顺序编排。
- Slot Chain:DefaultProcessSlotChain(first) -> NodeSelectorSlot -> ClusterBuilderSlot -> LogSlot -> StatisticSlot -> AuthoritySlot -> SystemSlot -> FlowSlot -> DegradeSlot(end)
- NodeSelectorSlot:负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级。
- ClusterBuilderSlot:用于存储资源的统计信息以及调用信息
- LogSlot:用于记录日志信息
- StatisticSlot:用于记录不同维度的runtime信息,Pass、Block、
- AuthoritySlot:根据黑白名单来进行控制
- SystemSlot:通过系统状态来控制总的入口流量
- FlowSlot:用于根据预设的限流规则,以及前面的slot统计状态,来进行限流
- DegradeSlot:通过统计信息以及预设的规则,来进熔断降级。
- SlotChain初始化**->执行链路:**
- SlotChain初始化:CtSph的lookProcessChain,该方法会根据资源先从一个静态Map中获取SlotChain,若取不到才会进行初始化。这也表明同一个资源会全局共享一个SlotChain。
This class holds metadata of current invocation。记录当前调用链的元数据,元数据包含:
- entranceNode:当前调用链的入口节点
- curEntry:调用链当前处理实体
- origin:当前调用链的调用源,如服务消费者名称或者源IP
每次SphU.entry() 或 SphO.entry()的调用流程需要在一个Context中执行,如果当前执行时还没有 context,那么框架会使用默认的 context,默认的 context 是通过 MyContextUtil.myEnter() 创建。若用户想自定义Context,调用ContextUtil.enter来实现。
- NodeSelectorSlot中的map,key为context的name而不是资源的name,原因是为了同一个资源多次调用,对应不同的Context,在各自执行完成后,再通过ClusterNode来进行合并。
- 【资源调用链路??】同一个context中对同一个resource进行多次entry()调用时,会形式一颗调用树,这个树是通过CtEntry之间的parent/child关系维护的。
## 1.4、凭证-Entry **Entry**是Sentinel用来标识是否进行限流降级的一个凭证,类似于一个token。每次执行`SphU.entry()` 或 `SphO.entry()`时都会返回一个Entry实体给调用者。告诉调用者如果正确返回了 `Entry`,那表示可以正常访问被sentinel保护的后方服务了,否则sentinel会抛出一个BlockException(如果是 `SphO.entry()`会返回false),这就表示调用者想要访问的服务被保护,即调用者本身被限流了。
-
Entry中包含了当前执行的一些基本信息:
- createTime:创建Entry实体的时间,主要后面用于计算rt
- curNode:当前执行节点
- originNode:当前执行流程源节点,通常是调用方的应用名称
- error:
- resourceWrapper:当前Entry关联的资源
-
当在一个上下文中多次调用了 SphU.entry()方法时,就会创建一个调用树,这个树的节点之间是通过parent和child关系维持的。需要注意的是:parent和child是在 CtSph类的一个私有内部类 CtEntry中定义的,CtEntry是 Entry的一个子类。
-
** SphU.entry 方法的第二个参数 EntryType 说的是这次请求的流量类型,共有两种类型:IN 和 OUT 。**
- IN:是指进入我们系统的入口流量,比如 http 请求或者是其他的 rpc 之类的请求。
- OUT:是指我们系统调用其他第三方服务的出口流量。
Node中保存了资源的实时统计数据,入passQps、blockQps、Rt等信息。Sentinel也是依据于这些数据才能实现限流降级等功能。Node的唯一统计实现类为StatisticNode。
- EntranceNode是每个上下文的入口节点类,该节点是直接挂在root下的,是全局唯一的,每一个context都会对应一个entranceNode。
public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
new ClusterNode());
- DefaultNode是记录当前调用的实时数据的,每个defaultNode都关联着一个资源和clusterNode,有着相同资源的defaultNode,他们关联着同一个clusterNode。
Metric是sentinel中用来进行实时数据统计的度量接口,Node就是通过Metric来进行数据统计的。而Metric本身也并没有统计的能力,他也是通过Window来进行统计的。
Metric的实现类ArrayMetric,其通过LeapArray来实现对窗口的统计。
Sentinel的一堆Slot构成了调用链路,从SphU.entry或者Sph0.entry开始,到lookProcessChain初始化slotchain,再调用ProcessorSlot的entry进入slot调用链。
每个Slot执行完逻辑后,都会调用fireEntry()方法,会触发下一个节点的entry()方法,下一个节点又会调用他的fireEntry,以此类推直到最后一个Slot,由此就形成了sentinel的责任链。
每次链调用一个资源,关联着一个Context,每个Context包含entranceNode、curEntry、originNode(名字)
- 再提一下几个核心的角色:
- Context
- Entry
- Node
- 创建:Context的创建由ContextUtil.trueEnter来实现。
protected static Context trueEnter(String name, String origin) {
//从ThreadLocal中获取Context
Context context = contextHolder.get();
if (context == null) {
//如果ThreadLocal中获取不到Context,则根据contextName从map中获取根节点,只要是相同的资源名,就能直接从map中获取到Node(入口节点)
Map localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
//当然context还没有入口节点,则判断已存在的个数大小,或者重新创建
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
try {
LOCK.lock();
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
//创建一个新的入口节点
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node.
// 将新的入口节点加入到ROOT节点下
Constants.ROOT.addChild(node);
Map newMap = new HashMap<>(contextNameNodeMap.size() + 1);
newMap.putAll(contextNameNodeMap);
newMap.put(name, node);
contextNameNodeMap = newMap;
}
}
} finally {
LOCK.unlock();
}
}
}
//创建一个新的Context,并设置Context的根节点,即设置EntranceNode
context = new Context(node, name);
context.setOrigin(origin);
//将Context保存到ThreadLocal中
contextHolder.set(context);
}
return context;
}
- 销毁
Context的销毁动作是调用ContextUtil.exit(),将对应的Context的对象清除掉。那什么时候才会触发Context销毁操作呢?
- 触发Context销毁:
- 一个是主动调用ContextUtil的exit()方法
- 二是当一个入口Entry要退出,执行该Entry的trueExit方法的时候,此时会触发ContextUtil.exit的方法。但是有一个前提,就是当前Entry的父Entry为null时,此时说明该Entry已经是最顶层的根节点了,可以清除context。【??销毁Context还会清除SlotChain】
- 创建Context时curEntry是null,只有name和entranceNode有值。
- **创建Entry:**开始构建树干节点,整体的主干,有点像一个双向链表。
class CtEntry extends Entry {
//父节点
protected Entry parent = null;
//子节点
protected Entry child = null;
protected ProcessorSlot
- **退出****Entry:**退出时会重新设置Context的curEntry。当该Entry是最顶层的一个入口时(**parent ****== **null),将会把ThreadLocal中保存的context也清除掉。
public void exit(int count, Object... args) throws ErrorEntryFreeException {
exitForContext(context, count, args);
}
protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
if (context != null) {
//不断递归退出当前curEntry,直到当前Entry
if (context.getCurEntry() != this) {
String curEntryNameInContext = context.getCurEntry() == null ? null : context.getCurEntry().getResourceWrapper().getName();
// Clean previous call stack.
CtEntry e = (CtEntry)context.getCurEntry();
while (e != null) {
//继续递归退出Entry
e.exit(count, args);
e = (CtEntry)e.parent;
}
String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
+ ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext, resourceWrapper.getName());
throw new ErrorEntryFreeException(errorMessage);
} else {
//SlotChain退出,后面分析
if (chain != null) {
chain.exit(context, resourceWrapper, count, args);
}
//用当然Entry的parent置为Context的当前Entry,并断开child
context.setCurEntry(parent);
if (parent != null) {
((CtEntry)parent).child = null;
}
//当parent为null时,标识已经到最顶层,conext销毁。
if (parent == null) {
// Default context (auto entered) will be exited automatically.
if (ContextUtil.isDefaultContext(context)) {
ContextUtil.exit();
}
}
// Clean the reference of context in current entry to avoid duplicate exit.
clearEntryContext();
}
}
}
通过CtEntry()构建了树干,那叶子节点怎么创建?
DefaultNode就是叶子节点,在叶子节点中保存着目标资源在当前状态下的统计信息。其叶子节点是由属性private volatile Set
- 获取当前上下文对应的DefaultNode,如果没有的话会为当前的调用新生成一个DefaultNode节点,它的作用是对资源进行各种统计度量以便进行流控;
- 将新创建的DefaultNode节点,添加到context中,作为「entranceNode」或者「curEntry.parent.curNode」的子节点;
- 将DefaultNode节点,添加到context中,作为「curEntry」的curNode
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
//创建叶子节点
node = new DefaultNode(resourceWrapper, null);
HashMap cacheMap = new HashMap(map.size());
cacheMap.putAll(map);
//以context name为key,将叶子节点缓存起来
cacheMap.put(context.getName(), node);
map = cacheMap;
// Build invocation tree
// 将叶子节点加到树干的当前节点的叶子链上的的尾部,作为叶子节点。
// 如果context的curEntry.parent.curNode为null,则添加到entranceNode中去
// 否则添加到context的curEntry.parent.curNode中去
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
//定位当前节点
context.setCurNode(node);
//调用下一个Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
Tip:图中的node0,node1,node2可能是相同的node,因为map的key是context的name,在同一个context中从map中获取的node是同一个。
**
NodeSelectorSlot中的逻辑。
具体流程就是创建叶子节点并构建调用树链。
NodeSelectorSlot的entry方法执行完之后,会调用fireEntry方法,此时会触发ClusterBuilderSlot的entry方法。
NodeSelectorSlot的职责比较简单,主要做了两件事:
- 为每个资源创建一个clusterNode,然后把clusterNode塞到DefaultNode中去
- 将clusterNode保持到全局的map中去,用资源作为map的key
PS:一个资源只有一个ClusterNode,但是可以有多个DefaultNode。
//记录每个资源的ClusterNode private static volatile MapclusterNodeMap = new HashMap<>();
### 2.3.2、StatistcSlot StatisticSlot负责来统计资源的实时状态,具体是采用[滑动窗口](#EBRt3)来实现的。
- 第一部分是entry方法,该方法首先会触发后续slot的entry方法,即SystemSlot、FlowSlot、DegradeSlot等的规则,如果规则不通过,就会抛出BlockException,则会在node中统计被block的数量。反之会在node中统计通过的请求数和线程数等信息。
- 第二部分是在exit方法中,当退出该Entry入口时,会统计rt的时间,并减少线程数。
SystemSlot就是根据总的请求统计信息,来做流控,主要是防止系统被搞垮。其主要依赖于Constants.ENTRY_NODE,它是一个全局的ClusterNode,该节点的值是在StatisticsSlot中进行统计的。
黑白名单校验,只要一条规则没通过就失败。
FlowSlot主要是根据前面StatisticsSlot统计好的信息,与设置的限流规则进行匹配校验,如果规则校验不通过则进行限流。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
//限流规则校验
checkFlow(resourceWrapper, context, node, count, prioritized);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
//FlowRuleChecker进行限流规则校验
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}
2.3.6、DegradeSlot
DegradeSlot主要是根据前面StatisticsSlot统计好的信息,与设置的降级规则进行匹配校验,如果规则校验不通过则进行降级。
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
throws Throwable {
//调用降级规则校验逻辑
DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
//获取降级的规则集合
Set rules = degradeRules.get(resource.getName());
//逐个规则校验,若触发降级规则,则抛出DegradeException异常
for (DegradeRule rule : rules) {
if (!rule.passCheck(context, node, count)) {
throw new DegradeException(rule.getLimitApp(), rule);
}
}
}
Sentinel通过一个由7个核心的Slot构建成的SlotChain提供能力,这些Slot各司其职,可以分为以下几种类型:
- 进行资源调用路径构造的NodeSelectorSlot和ClusterBuilderSlot
- 进行资源的实时状态统计的StatisticsSlot
- 进行系统保护,限流,降级等规则校验的SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot。
- ArrayMetric -> Metric: 用于记录请求的各种指标数据(success、pass、block等),其中包含一个LeapArray,用来计算各项指标。
- LeapArray: 各项数据指标统计基本数据模型,其用滑动窗口 采样来实现数据统计。真实用到的是"BucketLeapArray"。
- WindowWrap:滑动窗口,包含滑动窗口大小、当前窗口开始时间、当前窗口内记录的数据。
- windowLengthInMs:一个单口滑动时间窗口的大小,单位是毫秒
- sampleCount:滑动窗口的个数
- intervalInMs:窗口的区间大小,单位毫秒
- AtomicReferenceArray
array:滑动窗口数组
//LeapArray.currentWindow public WindowWrap3.4、滑动窗口使用currentWindow(long timeMillis) { //计算timeId,算出当前窗口在采样窗口数组中的索引idx int idx = calculateTimeIdx(timeMillis); //根据当前时间计算出当前时间窗口应该对应的开始时间(毫秒) long windowStart = calculateWindowStart(timeMillis); while (true) { //在采样窗口内获取idx对应的时间窗口 WindowWrap old = array.get(idx); if (old == null) { //如果idx对应的时间窗口不存在了,则以windowStart为基础创建一个新的时间窗口。将它插入到array的第idx个位置。 WindowWrap window = new WindowWrap (windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); if (array.compareAndSet(idx, null, window)) { return window; } else { // Contention failed, the thread will yield its time slice to wait for bucket available. Thread.yield(); } } else if (windowStart == old.windowStart()) { //如果当前窗口的开始时间time与old的开始时间相等,那么说明old就是当前时间窗口,直接返回old return old; } else if (windowStart > old.windowStart()) { //若开始时间已经大于已有的时间窗口开始时间,表示old窗口已经过时,将其开始时间更新为当前开始时间。 //在下次循环时,将会直接返回。 if (updateLock.tryLock()) { try { // Successfully get the update lock, now we reset the bucket. return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { // Contention failed, the thread will yield its time slice to wait for bucket available. Thread.yield(); } } else if (windowStart < old.windowStart()) { //当前窗口时间小于old的时间,这种情况基本不存在,因为当然时间是现在时间,old是一个过去时. return new WindowWrap (windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } }
在StatisticNode中两个Metric,在构建DefaultNode的时候,会触发创建ArrayMetric,最后触发到LeapArray。
在统计过程中获取一些指标如passQpsrt等时,会调用LeapArray.currentWindow()获得滑动窗口即可获得滑动窗口内的数据。
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
4、注解SentinelResource
4.1、SentinelResourceAspect
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
@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) {
throw new IllegalStateException("Wrong state for SentinelResource annotation");
}
String resourceName = getResourceName(annotation.value(), originMethod);
EntryType entryType = annotation.entryType();
Entry entry = null;
try {
//触发Sentinel 调用链
entry = SphU.entry(resourceName, entryType, 1, pjp.getArgs());
Object result = pjp.proceed();
return result;
} catch (BlockException ex) {
return handleBlockException(pjp, annotation, ex);
} catch (Throwable ex) {
throw ex;
} finally {
if (entry != null) {
entry.exit(1, pjp.getArgs());
}
}
}
}
**
- 将SentinelResourceAspect注入到当前工程的Spring容器中。
- 使用@SentinelResource定义资源,该注解粒度为方法。
- FlowSlot
- FlowRuleFlowRuleManager
- TrafficShapingController
- DefaultController
- RateLimitController
- WarmUpController
- WarmUpRateLimiterController
创建规则
- 实现原理:采用dubbo filter机制,拦截请求,在invoke方法中触发sentinel的逻辑,若被限流降级(抛异常:FlowException/BloackException)则报错。
- 主要实现类:SentinelDubboProviderFilter、SentinelDubboConsumerFilter、DubboAppContextFilter、DubboFallbackRegistry
- DubboAppContextFilter:用于记录Dubbo请求上下文信息【属于consumer】
- SentinelDubboProviderFilter:服务提供者Filter,主要触发服务提供者端的Sentinel配置并将资源信息记录到Sentinel的上下文中,资源流向被定义为EntryType._IN。_
- SentinelDubboConsumerFilter:服务消费者Filter,主要触发服务消费者端的Sentinel配置,资源流向被定义为EntryType._OUT_。
- DubboFallbackRegistry:触发Sentinel限流降级配置,抛出BlockException异常时,默认的失败机制。内部将异常包装成SentinelRpcException返回。



