栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Sentinel原理

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Sentinel原理

1、核心概念

1.1、资源-Resource
  • 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: 是对方法调用的包装。
1.2、槽-Slot

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。

1.3、上下文-Context

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:是指我们系统调用其他第三方服务的出口流量。

1.5、节点-Node

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。


1.6、度量-Metric

Metric是sentinel中用来进行实时数据统计的度量接口,Node就是通过Metric来进行数据统计的。而Metric本身也并没有统计的能力,他也是通过Window来进行统计的。
Metric的实现类ArrayMetric,其通过LeapArray来实现对窗口的统计。

2、Sentinel调用链

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


2.1、Context创建与销毁
  • 创建: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】

2.2、调用链树

2.2.1、树干节点
  • 创建Context时curEntry是null,只有name和entranceNode有值。

  • **创建Entry:**开始构建树干节点,整体的主干,有点像一个双向链表。
class CtEntry extends Entry {
	
    //父节点
    protected Entry parent = null;
    //子节点
    protected Entry child = null;

    protected ProcessorSlot chain;
    //上下文
    protected Context context;

    CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot chain, Context context) {
        super(resourceWrapper);
        this.chain = chain;
        this.context = context;
        //获取「上下文」中上一次的入口(因为context第一次没有curEntry,若有值则是上一次的)
		this.parent = context.getCurEntry();
        if (parent != null) {
            // 然后将当前入口设置为上一次入口的子节点
            ((CtEntry)parent).child = this;
        }
         设置「上下文」的当前入口为该类本身
        context.setCurEntry(this);
    }
}
 

  • **退出****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();
        }
    }
}


2.2.2、叶子节点

通过CtEntry()构建了树干,那叶子节点怎么创建?

DefaultNode就是叶子节点,在叶子节点中保存着目标资源在当前状态下的统计信息。其叶子节点是由属性private volatile Set childList = new HashSet<>();来记录的。实际创建叶子节点是在NodeSelectorSlot里。

  • 获取当前上下文对应的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是同一个。
**

2.3.3、保存叶子节点

NodeSelectorSlot中的逻辑。

2.3、Slot

2.3.0、NodeSelectorSlot

具体流程就是创建叶子节点并构建调用树链。

2.3.1、ClusterBuilderSlot

NodeSelectorSlot的entry方法执行完之后,会调用fireEntry方法,此时会触发ClusterBuilderSlot的entry方法。
    NodeSelectorSlot的职责比较简单,主要做了两件事:

  • 为每个资源创建一个clusterNode,然后把clusterNode塞到DefaultNode中去
  • 将clusterNode保持到全局的map中去,用资源作为map的key

PS:一个资源只有一个ClusterNode,但是可以有多个DefaultNode。

//记录每个资源的ClusterNode
private static volatile Map clusterNodeMap = new HashMap<>();



### 2.3.2、StatistcSlot StatisticSlot负责来统计资源的实时状态,具体是采用[滑动窗口](#EBRt3)来实现的。
  • 第一部分是entry方法,该方法首先会触发后续slot的entry方法,即SystemSlot、FlowSlot、DegradeSlot等的规则,如果规则不通过,就会抛出BlockException,则会在node中统计被block的数量。反之会在node中统计通过的请求数和线程数等信息。
  • 第二部分是在exit方法中,当退出该Entry入口时,会统计rt的时间,并减少线程数。
2.3.3、SystemSlot

SystemSlot就是根据总的请求统计信息,来做流控,主要是防止系统被搞垮。其主要依赖于Constants.ENTRY_NODE,它是一个全局的ClusterNode,该节点的值是在StatisticsSlot中进行统计的。

2.3.4、AuthoritySlot

黑白名单校验,只要一条规则没通过就失败。

2.3.5、FlowSlot

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。
3、滑动窗口

3.1、成员
  • ArrayMetric -> Metric: 用于记录请求的各种指标数据(success、pass、block等),其中包含一个LeapArray,用来计算各项指标。
  • LeapArray: 各项数据指标统计基本数据模型,其用滑动窗口 采样来实现数据统计。真实用到的是"BucketLeapArray"。
  • WindowWrap:滑动窗口,包含滑动窗口大小、当前窗口开始时间、当前窗口内记录的数据。
3.2、属性
  • windowLengthInMs:一个单口滑动时间窗口的大小,单位是毫秒
  • sampleCount:滑动窗口的个数
  • intervalInMs:窗口的区间大小,单位毫秒
  • AtomicReferenceArray array:滑动窗口数组

3.3、滑动窗口实现
//LeapArray.currentWindow
public WindowWrap 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));
        }
    }
}

3.4、滑动窗口使用

在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());
            }
        }
    }
}

**

4.2、注解使用
  • 将SentinelResourceAspect注入到当前工程的Spring容器中。
  • 使用@SentinelResource定义资源,该注解粒度为方法。
5、限流规则
  • FlowSlot
  • FlowRuleFlowRuleManager
  • TrafficShapingController
    • DefaultController
    • RateLimitController
    • WarmUpController
    • WarmUpRateLimiterController

5.1、规则管理

创建规则

5.2、规则流控策略

6、适配器

6.1、dubbo-adapter
  • 实现原理:采用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返回。
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号