Sentinel核心源码解析

本文主要来解析Sentinel的核心源码,基于当前最新的release版本1.8.0,如果你尚未了解Sentinel的核心功能,可以查看Sentinel的官方文档或者这篇文章Sentinel核心功能实战

1、Sentinel案例解析

    public UserOrder getUserOrderByUserId(Long userId) {
    
    
        ContextUtil.enter("UserService");
        Entry entry = null;
        try {
    
    
            entry = SphU.entry("getUserOrderByUserId", EntryType.IN);
            //根据用户id查询用户信息
            User userInfo = getUserById(userId);
            //根据用户id查询订单信息
            List<Order> orderList = orderService.getOrderByUserId(userId);
            return UserOrder.builder()
                    .user(userInfo)
                    .orderList(orderList)
                    .build();
        } catch (BlockException ex) {
    
    
            log.error("系统繁忙", ex);
            throw new RuntimeException("系统繁忙");
        } finally {
    
    
            if (entry != null) {
    
    
                entry.exit();
            }
        }
    }
    public List<Order> getOrderByUserId(Long userId) {
    
    
        Entry entry = null;
        try {
    
    
            entry = SphU.entry("getOrderByUserId");
            //根据用户id查询订单信息
            return new ArrayList<>();
        } catch (BlockException ex) {
    
    
            log.error("系统繁忙", ex);
            return null;
        } finally {
    
    
            if (entry != null) {
    
    
                entry.exit();
            }
        }
    }

1)Context代表一个调用链的入口,Context实例设置在ThreadLocal中,所以它是跟着线程走的,如果要切换线程,需要手动通过ContextUtil.runOnContext(context, f)切换

ContextUtil.enter()有两个参数:

第一个参数是context name,它代表调用链的入口,作用是为了区分不同的调用链路,默认为sentinel_default_context

第二个参数代表调用方标识origin,目前它有两个作用,一是用于黑白名单的授权控制,二是可以用来统计诸如从应用A发起的对当前应用xxx接口的调用,目前这个数据会被统计,但是dashboard中并不展示

2)进入BlockException异常分支,代表该次请求被流量控制规则限制了,一般会让代码走入到熔断降级的逻辑里面。BlockException有好多个子类,如DegradeException、FlowException等,也可以catch具体的子类来进行处理

3)SphU.entry()方法:

第一个参数标识资源,通常就是接口标识,对于数据统计、规则控制等,一般都是在这个粒度上进行的,根据这个字符串来唯一标识,它会被包装成ResourceWrapper实例

第二个参数标识资源的类型,EntryType.IN代表这个是入口流量,比如我们的接口对外提供服务,那么我们通常就是控制入口流量;EntryType.OUT代表出口流量,比如上面的getOrderByUserId()方法(没写默认就是OUT),它的业务需要调用订单服务,像这种情况,压力其实都在订单服务中,那么我们就指定它为出口流量

SystemSlot类中,它用于实现自适应限流,根据系统健康状态来判断是否要限流,如果是OUT类型,由于压力在外部系统中,所以就不需要执行这个规则

4)getOrderByUserId()方法中嵌套使用了Entry。如果我们在一个方法中写的话,要注意内层的Entry先exit,才能做外层的exit,否则会抛出异常。源码角度来看,是在Context实例中,保存了当前的Entry实例

扫描二维码关注公众号,回复: 12964023 查看本文章

2、ContextUtil#enter

    public static Context enter(String name, String origin) {
    
    
        if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
    
    
            throw new ContextNameDefineException(
                "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
        }
        return trueEnter(name, origin);
    }

    protected static Context trueEnter(String name, String origin) {
    
    
      	//ThreadLocal<Context> contextHolder
        Context context = contextHolder.get();
        if (context == null) {
    
    
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
            DefaultNode node = localCacheNameMap.get(name);
            if (node == null) {
    
    
                if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
    
    
                    setNullContext();
                    return NULL_CONTEXT;
                } else {
    
    
                    LOCK.lock();
                    try {
    
    
                        node = contextNameNodeMap.get(name);
                        if (node == null) {
    
    
                            if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
    
    
                                setNullContext();
                                return NULL_CONTEXT;
                            } else {
    
    
                              	//初始化EntranceNode实例
                                node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                                //ROOT_ID为machine-root
                                Constants.ROOT.addChild(node);

                                Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                                newMap.putAll(contextNameNodeMap);
                                newMap.put(name, node);
                                contextNameNodeMap = newMap;
                            }
                        }
                    } finally {
    
    
                        LOCK.unlock();
                    }
                }
            }
            context = new Context(node, name);
            context.setOrigin(origin);
            contextHolder.set(context);
        }

        return context;
    }

如果不显式调用ContextUtil.enter()方法的话,那root就只有一个default子节点sentinel_default_context

ContextUtil.enter("UserService")实际上会添加名为UserService的EntranceNode节点,可以得到下面这棵树:

在这里插入图片描述

Context代表线程执行的上下文,在Sentinel中,对于一个新的context name,Sentinel会往树中添加一个EntranceNode实例。它的作用是为了区分调用链路,标识调用入口。在Sentinel Dashboard中,可以很直观地看出调用链路

在这里插入图片描述

3、SphU#entry

SphU.entry()最终会调用CtSph类中的entryWithPriority()方法,详细代码如下:

    public static Entry entry(String name) throws BlockException {
    
    
        return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
    }
public class CtSph implements Sph {
    
    

    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    
    
        StringResourceWrapper resource = new StringResourceWrapper(name, type);
        return entry(resource, count, args);
    }
  
    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
    
    
        return entryWithPriority(resourceWrapper, count, false, args);
    }
  
    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
    
    
      
      	//从ThreadLocal中获取Context实例
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
    
    
            return new CtEntry(resourceWrapper, null, context);
        }

      	//如果不显式调用ContextUtil.enter(),会进入到默认的context中
        if (context == null) {
    
    
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        //Sentinel的全局开关,Sentinel提供了接口让用户可以在Dashboard开启或关闭
        if (!Constants.ON) {
    
    
            return new CtEntry(resourceWrapper, null, context);
        }

      	//用于构建一个责任链,入参是resource,资源的唯一标识是resource name(责任链模式)
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        if (chain == null) {
    
    
            return new CtEntry(resourceWrapper, null, context);
        }

      	//执行这个责任链 如果抛出BlockException,说明链上的某一环拒绝了该请求
      	//把这个异常往上层业务层抛,业务层处理BlockException应该进入到熔断降级逻辑中
        Entry e = new CtEntry(resourceWrapper, chain, context);
        try {
    
    
            chain.entry(context, resourceWrapper, null, count, prioritized, args);
        } catch (BlockException e1) {
    
    
            e.exit(count, args);
            throw e1;
        } catch (Throwable e1) {
    
    
            RecordLog.info("Sentinel unexpected exception", e1);
        }
        return e;
    }  

lookProcessChain()用于构建一个责任链。Sentinel的处理核心都在这个责任链中,链中每一个节点是一个Slot实例,这个链通过BlockException异常来告知调用入口最终的执行情况

Sentinel的核心骨架将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。核心结构如下图:

在这里插入图片描述

默认的DefaultSlotChainBuilder生成的责任链,详细代码如下:

public class DefaultSlotChainBuilder implements SlotChainBuilder {
    
    

    @Override
    public ProcessorSlotChain build() {
    
    
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        for (ProcessorSlot slot : sortedSlotList) {
    
    
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
    
    
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }

            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

使用SPI机制加载ProcessorSlot的所有子类,加载的顺序取决于子类上注解@SpiOrder的值,resources/META-INF/services目录下com.alibaba.csp.sentinel.slotchain.ProcessorSlot的文件内容如下:

# Sentinel default ProcessorSlots
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot
com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot
com.alibaba.csp.sentinel.slots.logger.LogSlot
com.alibaba.csp.sentinel.slots.statistic.StatisticSlot
com.alibaba.csp.sentinel.slots.block.authority.AuthoritySlot
com.alibaba.csp.sentinel.slots.system.SystemSlot
com.alibaba.csp.sentinel.slots.block.flow.FlowSlot
com.alibaba.csp.sentinel.slots.block.degrade.DegradeSlot

接下来,就按照默认的DefaultSlotChainBuilder生成的责任链往下看源码

对于相同的resource,使用同一个责任链实例,不同的resource,使用不同的责任链实例

4、NodeSelectorSlot

NodeSelectorSlot负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级

在这里插入图片描述

@SpiOrder(-10000)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
    
    
  
  	//key是context name,value是DefaultNode实例
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

    @Override
    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) {
    
    
                  	//初始化DefaultNode
                    node = new DefaultNode(resourceWrapper, null);
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;
                    // Build invocation tree
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }

      	//设置Context的当前Node
        context.setCurNode(node);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }  

责任链实例和resource name相关,和线程无关,所以当处理同一个resource的时候,会进入到同一个NodeSelectorSlot实例中

所以NodeSelectorSlot主要就是要处理:不同的context name,同一个resource name的情况

结合前面讲解的那棵树,可以得出下面这棵树:

在这里插入图片描述

5、ClusterBuilderSlot

ClusterBuilderSlot用于存储资源的统计信息以及调用者信息,例如该资源的RT、QPS、thread count等等,这些信息将用作为多维度限流、降级的依据

在这里插入图片描述

@SpiOrder(-9000)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    
  
    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

    private static final Object lock = new Object();

    private volatile ClusterNode clusterNode = null;

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args)
        throws Throwable {
    
    
        if (clusterNode == null) {
    
    
            synchronized (lock) {
    
    
                if (clusterNode == null) {
    
    
                  	//初始化ClusterNode
                    clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
                    HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }
        node.setClusterNode(clusterNode);

        if (!"".equals(context.getOrigin())) {
    
    
          	//初始化originNode
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }  

每一个resource会对应一个ClusterNode实例,如果不存在,就创建一个实例

ClusterNode是用来做数据统计的。比如getUserOrderByUserId这个接口,由于从不同的context name中开启调用链,它有多个DefaultNode实例,但是只有一个ClusterNode,通过这个实例,我们可以知道这个接口现在的QPS是多少

在这里插入图片描述

origin代表调用方标识,当设置了origin的时候,这里会额外生成一个StatisticNode实例,挂在ClusterNode上

getUserOrderByUserId这个接口接收到了来自application-a和application-b两个应用的请求,那么树会变成下面这样:

在这里插入图片描述

它的作用是用来统计从不同来源过来的访问getUserOrderByUserId这个接口的信息

截止到这里Sentinel中各种统计节点都介绍完了,下面来总结下:

  • EntranceNode:入口节点,特殊的链路节点,对应某个Context入口的所有调用数据。Constants.ROOT节点也是入口节点
  • DefaultNode:链路节点,用于统计调用链路上某个资源的数据,维持树状结构
  • ClusterNode:簇点,用于统计每个资源全局的数据(不区分调用链路),以及存放该资源的按来源区分的调用数据(类型为 StatisticNode)。特别地,Constants.ENTRY_NODE节点用于统计全局的入口资源数据
  • StatisticNode:最为基础的统计节点,包含秒级和分钟级两个滑动窗口结构

构建的时机:

  • EntranceNode在ContextUtil.enter()的时候就创建了,然后塞到Context里面
  • NodeSelectorSlot根据context创建DefaultNode,然后set curNode to context
  • ClusterBuilderSlot首先根据resourceName创建ClusterNode,并且set clusterNode to defaultNode;然后再根据origin创建来源节点(类型为StatisticNode),并且set originNode to curEntry

几种Node的维度(数目):

  • EntranceNode的维度是context,存在ContextUtil类的contextNameNodeMap里面
  • DefaultNode的维度是resource * context,存在每个NodeSelectorSlot的map里面
  • ClusterNode的维度是resource
  • StatisticNode的维度是resource * origin,存在每个ClusterNode的originCountMap里面

6、LogSlot

在这里插入图片描述

@SpiOrder(-8000)
public class LogSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode obj, int count, boolean prioritized, Object... args)
        throws Throwable {
    
    
        try {
    
    
            fireEntry(context, resourceWrapper, obj, count, prioritized, args);
        } catch (BlockException e) {
    
    
            EagleEyeLogUtil.log(resourceWrapper.getName(), e.getClass().getSimpleName(), e.getRuleLimitApp(),
                context.getOrigin(), count);
            throw e;
        } catch (Throwable e) {
    
    
            RecordLog.warn("Unexpected entry exception", e);
        }

    }

如果抛出了BlockException,这里调用了EagleEyeLogUtil.log()方法,将被设置的规则block的信息记录到日志文件sentinel-block.log中,记录哪些接口被规则挡住了

7、StatisticSlot&滑动窗口

StatisticSlot用于记录、统计不同纬度的runtime指标监控信息

在这里插入图片描述

@SpiOrder(-7000)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
    
    
        try {
    
    
            //先执行其他的处理器逻辑,执行完成后收集统计信息
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            //累加线程数threadNum,累加通过的request数量
          	//对于QPS统计,使用滑动窗口;而对于线程并发的统计,使用了LongAdder
            node.increaseThreadNum();
            node.addPassRequest(count);

            if (context.getCurEntry().getOriginNode() != null) {
    
    
                //如果originNode存在(类型为StatisticNode),则也需要增加originNode的线程数和请求通过数
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
    
    
                //如果资源包装类型是IN的话,则需要累加ENTRY_NODE的线程数和请求通过数
              	//ENTRY_NODE是sentinel全局的统计节点(类型为ClusterNode),用于后续系统规则检查
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest(count);
            }

            //循环处理注册了ProcessorSlotEntryCallback的StatisticSlot
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
    
    
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (PriorityWaitException ex) {
    
    
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
    
    
                // Add count for origin node.
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
    
    
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseThreadNum();
            }
            // Handle pass event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
    
    
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (BlockException e) {
    
    
            // Blocked, set block exception to current entry.
            context.getCurEntry().setBlockError(e);

            // Add block count.
            node.increaseBlockQps(count);
            if (context.getCurEntry().getOriginNode() != null) {
    
    
                context.getCurEntry().getOriginNode().increaseBlockQps(count);
            }

            if (resourceWrapper.getEntryType() == EntryType.IN) {
    
    
                // Add count for global inbound entry node for global statistics.
                Constants.ENTRY_NODE.increaseBlockQps(count);
            }

            // Handle block event with registered entry callback handlers.
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
    
    
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }

            throw e;
        } catch (Throwable e) {
    
    
            // Unexpected internal error, set error to current entry.
            context.getCurEntry().setError(e);

            throw e;
        }
    }
  
    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    
    
        Node node = context.getCurNode();

        if (context.getCurEntry().getBlockError() == null) {
    
    
            //计算响应时间,通过当前时间-CurEntry的创建时间取毫秒值
            long completeStatTime = TimeUtil.currentTimeMillis();
            context.getCurEntry().setCompleteTimestamp(completeStatTime);
            long rt = completeStatTime - context.getCurEntry().getCreateTimestamp();

            Throwable error = context.getCurEntry().getError();

            recordCompleteFor(node, count, rt, error);
            recordCompleteFor(context.getCurEntry().getOriginNode(), count, rt, error);
            if (resourceWrapper.getEntryType() == EntryType.IN) {
    
    
                recordCompleteFor(Constants.ENTRY_NODE, count, rt, error);
            }
        }

        // Handle exit event with registered exit callback handlers.
        Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
        for (ProcessorSlotExitCallback handler : exitCallbacks) {
    
    
            handler.onExit(context, resourceWrapper, count, args);
        }

        fireExit(context, resourceWrapper, count);
    }  
  
    private void recordCompleteFor(Node node, int batchCount, long rt, Throwable error) {
    
    
        if (node == null) {
    
    
            return;
        }
      	//新增响应时间和成功数
        node.addRtAndSuccess(rt, batchCount);
      	//线程数减1
        node.decreaseThreadNum();

        if (error != null && !(error instanceof BlockException)) {
    
    
            node.increaseExceptionQps(batchCount);
        }
    }  

数据统计的代码在StatisticNode中

public class StatisticNode implements Node {
    
    

    private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
        IntervalProperty.INTERVAL);

    private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);

    //使用AtomicInteger来统计当前线程数
    private LongAdder curThreadNum = new LongAdder();

从上面的代码也可以知道,Sentinel统计了两个维度的数据,下面来看下实现类ArrayMetric的源码设计

public class ArrayMetric implements Metric {
    
    

    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
    
    
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
    
    
        if (enableOccupy) {
    
    
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
    
    
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }

ArrayMetric的内部是一个LeapArray,以分钟维度统计的使用来说,它使用子类BucketLeapArray实现

public abstract class LeapArray<T> {
    
    

    protected int windowLengthInMs;
    protected int sampleCount;
    protected int intervalInMs;
    private double intervalInSecond;

    protected final AtomicReferenceArray<WindowWrap<T>> array;
  
  	//对于分钟维度的设置,sampleCount为60,intervalInMs为60*1000
		public LeapArray(int sampleCount, int intervalInMs) {
    
    
        AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
        AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
        AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

        //单个窗口长度,这里是1000ms
        this.windowLengthInMs = intervalInMs / sampleCount;
      	//一轮总时长60000ms
        this.intervalInMs = intervalInMs;
        this.intervalInSecond = intervalInMs / 1000.0;
       	//60个窗口
        this.sampleCount = sampleCount;

        this.array = new AtomicReferenceArray<>(sampleCount);
    }

它的内部核心是一个数组array,它的长度为60,也就是有60个窗口,每个窗口长度为1秒,刚好一分钟走完一轮。然后下一轮开启覆盖操作

在这里插入图片描述

每个窗口是一个WindowWrap类实例

  • 添加数据的时候,先判断当前走到哪个窗口了(当前时间(s) % 60即可),然后需要判断这个窗口是否是过期数据,如果是过期数据(窗口代表的时间距离当前已经超过1分钟),需要先重置这个窗口实例的数据
  • 统计数据同理,如统计过去一分钟的QPS数据,就是将每个窗口的值相加,当中需要判断窗口数据是否是过期数据,即判断窗口的WindowWrap实例是否是一分钟内的数据

核心逻辑都封装在了currentWindow(long timeMillis)values(long timeMillis)方法中

添加数据的时候,我们要先获取操作的目标窗口,也就是currentWindow()这个方法,Sentinel 在这里处理初始化和过期重置的情况:

public abstract class LeapArray<T> {
    
    
  
		public WindowWrap<T> currentWindow(long timeMillis) {
    
    
        if (timeMillis < 0) {
    
    
            return null;
        }

      	//获取窗口下标
        int idx = calculateTimeIdx(timeMillis);
        //计算该窗口的理论开始时间
        long windowStart = calculateWindowStart(timeMillis);

      	//嵌套在一个循环中,因为有并发的情况
        while (true) {
    
    
            WindowWrap<T> old = array.get(idx);
            if (old == null) {
    
    
              	//窗口未实例化的情况,使用CAS来设置该窗口实例
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
    
    
                    return window;
                } else {
    
    
                   	//存在竞争
                    Thread.yield();
                }
            } else if (windowStart == old.windowStart()) {
    
    
                //当前数组中的窗口没有过期
                return old;
            } else if (windowStart > old.windowStart()) {
    
    
                //该窗口已过期,重置窗口的值 使用一个锁来控制并发
                if (updateLock.tryLock()) {
    
    
                    try {
    
    
                        return resetWindowTo(old, windowStart);
                    } finally {
    
    
                        updateLock.unlock();
                    }
                } else {
    
    
                    Thread.yield();
                }
            } else if (windowStart < old.windowStart()) {
    
    
                //正常情况都不会走到这个分支,异常情况其实就是时钟回拨,这里返回一个WindowWrap是容错
                return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }

获取数据,使用的是values()方法,这个方法返回有效的窗口中的数据:

    public List<T> values(long timeMillis) {
    
    
        if (timeMillis < 0) {
    
    
            return new ArrayList<T>();
        }
        int size = array.length();
        List<T> result = new ArrayList<T>(size);

        for (int i = 0; i < size; i++) {
    
    
            WindowWrap<T> windowWrap = array.get(i);
          	//过滤掉过期数据,判断当前窗口的数据是否是60秒内的
            if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
    
    
                continue;
            }
            result.add(windowWrap.value());
        }
        return result;
    }

StatisticNode类注解总结了数据统计的原理

Sentinel使用滑动窗口来记录和统计实时调用数据

  • 当第一个请求到来,Sentinel会创建一个特殊的时间片(time-span)去保存运行时的数据,比如:响应时间(rt)、QPS、block request,在这里叫做滑动窗口(window bucket),这个滑动窗口通过sample count定义。Sentinel通过滑动窗口有效的数据来决定当前请求是否通过,滑动窗口将记录所有的QPS,将其与规则中定义的阈值进行比较
  • 不同的请求进来,根据不同的时间存放在不同滑动窗口中
  • 请求不断的进入系统,先前的滑动窗口将会过期无效

接下来要介绍的几个Slot,需要通过Dashboard进行开启,因为需要配置规则

8、AuthoritySlot

AuthoritySlot根据配置的黑白名单和调用来源信息,来做黑白名单控制

在这里插入图片描述

在Sentinel Dashboard上新增授权规则:

在这里插入图片描述

@SpiOrder(-6000)
public class AuthoritySlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
        throws Throwable {
    
    
      	//校验黑白名单授权
        checkBlackWhiteAuthority(resourceWrapper, context);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
  
    void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
    
    
        Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();

        if (authorityRules == null) {
    
    
            return;
        }

      	//根据资源名获取授权规则
        Set<AuthorityRule> rules = authorityRules.get(resource.getName());
        if (rules == null) {
    
    
            return;
        }

        for (AuthorityRule rule : rules) {
    
    
          	//如果passCheck校验返回false,抛出AuthorityException
            if (!AuthorityRuleChecker.passCheck(rule, context)) {
    
    
                throw new AuthorityException(context.getOrigin(), rule);
            }
        }
    }  
final class AuthorityRuleChecker {
    
    

    static boolean passCheck(AuthorityRule rule, Context context) {
    
    
        String requester = context.getOrigin();

        if (StringUtil.isEmpty(requester) || StringUtil.isEmpty(rule.getLimitApp())) {
    
    
            return true;
        }

        //匹配的时候根据origin判断
        int pos = rule.getLimitApp().indexOf(requester);
        boolean contain = pos > -1;

        if (contain) {
    
    
            boolean exactlyMatch = false;
            String[] appArray = rule.getLimitApp().split(",");
            for (String app : appArray) {
    
    
                if (requester.equals(app)) {
    
    
                    exactlyMatch = true;
                    break;
                }
            }

            contain = exactlyMatch;
        }

        int strategy = rule.getStrategy();
        if (strategy == RuleConstant.AUTHORITY_BLACK && contain) {
    
    
            return false;
        }

        if (strategy == RuleConstant.AUTHORITY_WHITE && !contain) {
    
    
            return false;
        }

        return true;
    }

    private AuthorityRuleChecker() {
    
    }
}

9、SystemSlot

SystemSlot通过系统的状态,例如load1等,来控制总的入口流量

在这里插入图片描述

在Sentinel Dashboard上新增系统保护规则包含以下几个类型:

在这里插入图片描述

@SpiOrder(-5000)
public class SystemSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    
    

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
    
    
        SystemRuleManager.checkSystem(resourceWrapper);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    
    
        fireExit(context, resourceWrapper, count, args);
    }

}

SystemSlot依赖于SystemRuleManager来做检查

public final class SystemRuleManager {
    
        

		public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
    
    
        if (resourceWrapper == null) {
    
    
            return;
        }
        //检查系统状态是否为false,如果为false,则代表不检查
      	//如果不配置SystemRule,则不检查
        if (!checkSystemStatus.get()) {
    
    
            return;
        }

        //系统检查状态,只检查外部调内部的接口状态,EntryType.IN内部调用外部接口不检查
        if (resourceWrapper.getEntryType() != EntryType.IN) {
    
    
            return;
        }

        //获取当前系统的QPS,根据ClusterNode的successQps计算successQps总数/时间 每秒成功的记录
        double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
        if (currentQps > qps) {
    
    
            throw new SystemBlockException(resourceWrapper.getName(), "qps");
        }

        //总线程数 
        int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
        if (currentThread > maxThread) {
    
    
            throw new SystemBlockException(resourceWrapper.getName(), "thread");
        }

      	//平均响应时长
        double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
        if (rt > maxRt) {
    
    
            throw new SystemBlockException(resourceWrapper.getName(), "rt");
        }

        //系统负载  
        if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
    
    
            if (!checkBbr(currentThread)) {
    
    
                throw new SystemBlockException(resourceWrapper.getName(), "load");
            }
        }

        //CPU使用率超过限制
        if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
    
    
            throw new SystemBlockException(resourceWrapper.getName(), "cpu");
        }
    }

由于系统的平均RT、当前线程数、QPS都可以从ENTRY_NODE中获得,所以限制代码非常简单,比较一下大小就可以了。如果超过阈值,抛出SystemBlockException

ENTRY_NODE是ClusterNode类型的,而ClusterNode对于RT、QPS都是统计的维度的数据

而对于系统负载和CPU资源的保护,核心类是SystemStatusListener

public class SystemStatusListener implements Runnable {
    
        

		@Override
    public void run() {
    
    
        try {
    
    
            OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
            currentLoad = osBean.getSystemLoadAverage();
            double systemCpuUsage = osBean.getSystemCpuLoad();
						
          	...
        } catch (Throwable e) {
    
    
            RecordLog.warn("[SystemStatusListener] Failed to get system metrics from JMX", e);
        }
    }

Sentinel通过调用OperatingSystemMXBean中的方法获取当前的系统负载和CPU使用率,Sentinel起了一个后台线程,每秒查询一次

10、FlowSlot

FlowSlot则用于根据预设的限流规则以及前面slot统计的状态,来进行流量控制

在这里插入图片描述
Sentinel FlowSlot限流源码解析

11、DegradeSlot

DegradeSlot通过统计信息以及预设的规则,来做熔断降级

在这里插入图片描述

Sentinel DegradeSlot熔断源码解析

参考

https://www.javadoop.com/post/sentinel

https://blog.csdn.net/qq924862077/article/details/97423682

https://github.com/alibaba/Sentinel/blob/master/doc/awesome-sentinel.md

https://github.com/alibaba/Sentinel/wiki/Sentinel%E5%B7%A5%E4%BD%9C%E4%B8%BB%E6%B5%81%E7%A8%8B

https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90

猜你喜欢

转载自blog.csdn.net/qq_40378034/article/details/113062015