Sentinel源码分析----调用流程总览

Sentinel 是什么?github描述如下

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

本文建立在会使用Sentinel的基础上,详细的介绍和使用不会展开,具体介绍和使用看:Sentinel介绍

一个简单的Demo如下:

        String resourceName = "资源名称";
        Entry entry = null;
        try {
            entry = SphU.entry(resourceName);
            run();
        } catch (BlockException ex) {
            throw ex;
        } catch (Throwable ex) {
            Tracer.trace(ex);
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }

这就是一个设置之后,run方法就会被Sentinel所监控起来,但是这时候,是没有任何效果的,因为没有告诉Sentinel需要去限制什么?在Sentinel中,这个叫做规则,即你需要设置好限制的规则,Sentinel会根据设置的规则去限制你的代码,即上面的run方法,那么下面来看下Sentinel的整个调用流程是如何。

SphU.entry方法为入口,一步步的跟进去

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

    //CtSph.java
    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, type);//2
        return entry(resource, count, args);//3
    }
  • 1:entry有很多重载的方法,如果不填,就会设置默认值,其他参数后续分析
  • 2:对于Sentinel来说,限制的是资源,这里将名称和EntryType构造成一个资源对象
  • 3:接着调用entry方法进行处理
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 {
        Context context = ContextUtil.getContext();// 1
        if (context instanceof NullContext) {
            return new CtEntry(resourceWrapper, null, context);
        }

        if (context == null) {
            context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());//2
        }

        if (!Constants.ON) {//3
            return new CtEntry(resourceWrapper, null, context);
        }

        ProcessorSlot chain = lookProcessChain(resourceWrapper);//4

        /*
         * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * so no rule checking will be done.
         */
        if (chain == null) {// 5
            return new CtEntry(resourceWrapper, null, context);
        }

        Entry e = new CtEntry(resourceWrapper, chain, context);
        try {
            chain.entry(context, resourceWrapper, null, count, prioritized, args);//6
        } 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;
    }
 
 
  • 1:从ThreadLocal中获取一个上下文对象,此时第一次调用,ThreadLocal为空。当然我们在demo中可以手动指定一个上下文,那么到这里就不会为空了
  • 2:第一次为空,所以需要进行一个Context的初始化
  • 3:一个全局开关,可供动态切换,如果关闭了,则后续就不会走规则校验
  • 4:画个重点!!!!这个ProcessorSlotChain是Sentinel整个流程的核心,相当于一个拦截器链,所有请求会经过拦截器链进行处理,一会分析
  • 5:chain为空,是由某种情况引起的,具体情况在lookProcessChain
  • 6:开始执行核心逻辑

获取上下文及初始化

private static ThreadLocal contextHolder = new ThreadLocal();
    public static Context getContext() {
        return contextHolder.get();
    }

从代码中看到,contextHolder在此之前没有做过初始化,那么会走到如下方法:

    //com.alibaba.csp.sentinel.CtSph
    MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
    //com.alibaba.csp.sentinel.CtSph.MyContextUtil
    private final static class MyContextUtil extends ContextUtil {
        static Context myEnter(String name, String origin, EntryType type) {
            return trueEnter(name, origin);
        }
    }
    //com.alibaba.csp.sentinel.context.ContextUtil
    protected static Context trueEnter(String name, String origin) {
        Context context = contextHolder.get();//1
        if (context == null) {
            Map localCacheNameMap = contextNameNodeMap;//2
            DefaultNode node = localCacheNameMap.get(name);//3
            if (node == null) {
                if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {// 4
                    setNullContext();
                    return NULL_CONTEXT;
                } else {
                    try {
                        LOCK.lock();
                        node = contextNameNodeMap.get(name);
                        if (node == null) {
                            if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {// 5
                                setNullContext();
                                return NULL_CONTEXT;
                            } else {// 6
                                node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                                // Add entrance node.
                                Constants.ROOT.addChild(node);

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

        return context;
    }
  • 1:首先这里从ThreadLocal中获取,这时还是空的
  • 2:contextNameNodeMap这里初始化的时候默认加了个name为Constants.CONTEXT_DEFAULT_NAME的节点进去,所以上述demo会走到7。这里
  • 3:假设我们自己指定了上下文名称(是否要指定上下文需要看情况),那么第一次进行,这里为空,会进入下面的判断
  • 4:localCacheNameMap.size()即上下文的数量(因为以contextName为key),Sentinel限制了上下文的数量是2000以下,如果大于2000会返回NULL_CONTEXT,在之前的处理中可以看到NULL_CONTEXT是直接返回的
  • 5:由于4的判断是在无锁的情况下进行的,所以需要在加锁条件下再进行一次判断(类似单例double check的模式),假设没有问题则会走到6
  • 6:这里创建了个EntranceNode节点,关于Sentinel中Node的问题会在后续文章分析
  • 7:创建了个上下文对象并且放入了ThreadLocal中,下次同一线程可以直接获取

注意点:

  1. 由于这个Node节点内部有许多信息,为了限制内存占用,会限制上下文的数量
  2. 有些情况不关心上下文,那么就如demo中一样,直接调用SphU.entry,那么这时候会指定一个默认的供其使用,如果需要区分上下文,那么则需要在SphU.entry之前调用ContextUtil.enter方法指定上下文

调用链的创建与触发

    ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);//1
        if (chain == null) {// 2
            synchronized (LOCK) {// 3
                chain = chainMap.get(resourceWrapper);// 4
                if (chain == null) {//5
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {// 6
                        return null;
                    }
                  
                    chain = SlotChainProvider.newSlotChain();// 7
                    Map newMap = new HashMap(
                        chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }
 
 
  • 1:上面的Node节点是和上下文关联的,而这个的ProcessorSlotChain是和资源关联的,即一个资源会有一个ProcessorSlotChain对象
  • 2~5:使用了double check,判断map中是否有该资源的ProcessorSlotChain对应
  • 6:同上面上下文的创建,控制内存
  • 7:初始化一个ProcessorSlotChain对象并放入map中

看下SlotChainProvider.newSlotChain方法

    public static ProcessorSlotChain newSlotChain() {
        if (builder != null) {// 第一次会为空,需要初始化
            return builder.build();
        }
        // 初始化builder
        resolveSlotChainBuilder();

        if (builder == null) {// 初始化后仍然为空,则设置为默认的Builder
            builder = new DefaultSlotChainBuilder();
        }
        // 通过builder创建ProcessorSlotChain
        return builder.build();
    }
    // 通过java SPI的机制获取对应的信息
    private static final ServiceLoader LOADER = 
            ServiceLoader.load(SlotChainBuilder.class);
    private static void resolveSlotChainBuilder() {
        List list = new ArrayList();
        boolean hasOther = false;
        // 获取SPI中配置的实现
        for (SlotChainBuilder builder : LOADER) {
            // 如果SPI的配置文件中自定义了实现
            if (builder.getClass() != DefaultSlotChainBuilder.class) {
                hasOther = true;
                list.add(builder);
            }
        }
        // 如果有多个自定义实现,则默认取第一个
        if (hasOther) {
            builder = list.get(0);
        } else {
            // 没有自定义实现那么取默认实现.
            builder = new DefaultSlotChainBuilder();
        }
    }

Sentinel中大量使用了Java 的SPI机制去进行一个扩展,这里就用来扩展Builder,如果我们需要自己去自定义一个Buidler,去排列调用链中的元素节点,那么可以参考Java SPI机制去配置,那么Sentinel就选择自定义的Builder去创建ProcessorSlotChain,而默认情况使用的是DefaultSlotChainBuilder,那么看下其build方法

    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new FlowSlot()); 
        chain.addLast(new DegradeSlot());
        return chain;
    }

可以看到DefaultSlotChainBuilder已经默认排列好了调用链中的节点,其实内部就类似一个拦截器链,Slot是拦截器链中的拦截器节点,每个节点的功能不同,具体功能如下:

  • NodeSelectorSlot:用于创建Node节点
  • ClusterBuilderSlot:用于创建ClusterNode节点
  • LogSlot:目前对于被规则限制的情况,交给了StatLogger处理,但是好像没啥效果?
  • StatisticSlot:用于统计当前流量通过的情况
  • SystemSlot:用于系统负载规则的处理
  • AuthoritySlot: 用于黑白名单规则的处理
  • FlowSlot:用于限流规则的处理
  • DegradeSlot:用于降级规则的处理

DefaultProcessorSlotChain这个调用链或者说拦截器链,一般来说是数组或者链表实现的,通过上面的addLast方法来看,应该用链表会比较合适(这个类似Netty的pipeline,有addLast的话,应该有addFirst,如果有addFirst的话,数组就不合适了,因为数组插入元素的话比较麻烦,而链表就比较容易了)

public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    AbstractLinkedProcessorSlot first = new AbstractLinkedProcessorSlot() {

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

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

    };
    AbstractLinkedProcessorSlot end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }

    @Override
    public void setNext(AbstractLinkedProcessorSlot next) {
        addLast(next);
    }
}
 
 

结构就是链表的结构,有头节点,尾节点,每个节点都有个next引用指向下一个节点,这里需要注意的是next引用它是在父类里的,这里可以类比一下Netty的pipeline,有少许不同,但是核心都差不多

还有个点需要看下,和Netty有点类似,以FlowSlot为例

public class FlowSlot extends AbstractLinkedProcessorSlot {

    @Override
    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);
    }
}

可以看到最后执行完checkFlow还调用了一次fireEntry,这个会继续往后触发,Netty的pipeline也是这样的触发形式

数据统计

上面介绍了几个Slot的作用,以常用的限流规则为例,我们在控制台配置限流规则:

Sentinel源码分析----调用流程总览_第1张图片
image.png

例如配置qps为10,那么在 FlowSlot会检查当前qps是否超过这个值,没超过则通过该请求,否则抛出异常,那么有个疑问, FlowSlot如何获取当前服务的一个qps或者说请求量呢?

这时候就轮到我们的主角StatisticSlot登场了,其entry方法如下

    @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);// 1

            node.increaseThreadNum();//2
            node.addPassRequest(count);//3

            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();//4
                context.getCurEntry().getOriginNode().addPassRequest(count);//5
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseThreadNum();//6
                Constants.ENTRY_NODE.addPassRequest(count);//7
            }

            // ....
        } catch (BlockException e) {
            // ....
            node.increaseBlockQps(count);//8
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseBlockQps(count);//9
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseBlockQps(count);//10
            }
            // ....
            throw e;
        } catch (Throwable e) {
            // ....
            node.increaseExceptionQps(count);//11
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseExceptionQps(count);//12
            }

            if (resourceWrapper.getType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseExceptionQps(count);//13
            }
            throw e;
        }
    }

上面是entry方法做的逻辑,主要关注的几点已经标注出来了,fireEntry这里是触发后续节点,从DefaultSlotChainBuilder#build方法中可以看到StatisticSlot后还有四个节点用来校验规则,即这里的fireEntry会触发规则的校验,规则校验通过则往下走,失败的走catch块。

从2~13的方法名称中可以知道这里进行了流量的统计,例如增加线程数->increaseThreadNum,增加通过的请求数->addPassRequest,增加block请求qps->increaseBlockQps,增加异常qps->increaseExceptionQps,到这里就能知道Sentinel是如何统计请求数的(Node的具体原理后续分析)。

结合上述调用链的执行,整个流程如下(省略部分Slot的处理):


Sentinel源码分析----调用流程总览_第2张图片
image.png

总结

Sentinel的保护措施是在一个Slot链中,Slot链有不同的节点,每个节点负责不同的事情,例如降级相关规则、系统负载相关规则、限流相关规则的处理,如果触发了某个规则(例如qps已经超过配置的规则),那么会抛出异常,而Slot链有个节点负责统计成功和异常的数量,然后这时候就不会执行保护的代码,达到一个保护的作用

你可能感兴趣的:(Sentinel源码分析----调用流程总览)