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<Object> 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<Context> contextHolder = new ThreadLocal<Context>();
    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<String, DefaultNode> 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<String, DefaultNode> newMap = new HashMap<String, DefaultNode>(
                                    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<Object> 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<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        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<SlotChainBuilder> LOADER = 
            ServiceLoader.load(SlotChainBuilder.class);
    private static void resolveSlotChainBuilder() {
        List<SlotChainBuilder> list = new ArrayList<SlotChainBuilder>();
        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<Object>() {

        @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<DefaultNode> {

    @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的作用,以常用的限流规则为例,我们在控制台配置限流规则:

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的处理):


image.png

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335