sentinel 是如何限流的

对 sentinel 的学习笔记

Posted by JerAxxxxxxx on April 26, 2021
5 min read

这一篇,来学习 sentinel 是如何对我们的资源进行限流等操作的。

在上一篇文章中了解到,使用 @SentinelResource 注解后,是通过切面的方式,调用了 SphU.entry() 方法,那么本章就来深挖一下这个方法具体做了些什么。

限流的入口

我们通过进入SphU.entry() 方法可以发现,其先是调用到了CtSph 类中的 entryWithType() 方法,经过对资源信息的封装后,最终调用的是CtSph.entryWithPriority() 方法。

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {
    	// 获取上下文对象
        Context context = ContextUtil.getContext();
        if (context instanceof NullContext) {
            // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
            // so here init the entry only. No rule checking will be done.
            return new CtEntry(resourceWrapper, null, context);
        }

        if (context == null) {
            // Using default context.
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        // Global switch is close, no rule checking will do.
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }
		// 获取规则处理链条
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

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

        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) {
            // This should not happen, unless there are errors existing in Sentinel internal.
            RecordLog.info("Sentinel unexpected exception", e1);
        }
        return e;
    }

该方法篇幅较长,其主要是从 ThreadLocal 中获取上下文对象,对空的情况做出默认的一些处理,然后通过资源装饰器,获取对应的处理链条。这里运用到了责任链模式,不同的任务交给不同的责任链去处理,最终调用每个链条的 entry() 方法。

获取上下文对象 Context

ContextUtil 类中维护的几个重要属性。

 ContextUtil 的重要属性

ContextUtil 在初始化的时候,会通过默认的上下文名称,生成一个 EntranceNode 对象,该对象提供了对请求通过与拒绝数及总数的统计的方法。并将该对象加入到 Constants.ROOT 的子节点中,而 Constants.ROOT 也是一个 node,一个全局的统计节点。

 Constants.ROOT

而最终 ContextUtil.getContext() 获取的是该线程的上下文对象。即 ThreadLocal.get().

获取责任链 ProcessorSlot

这里 lookProcessChain(resourceWrapper) 是个非常重要的方法。

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    	// 首先从 map 中获取是否存在该资源的插槽链
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);
        if (chain == null) {
            synchronized (LOCK) {
                chain = chainMap.get(resourceWrapper);
                if (chain == null) {
                    // 这里使用了并发编程中非常经典的 double check
                    // 插槽链的最大长度为 6000,如果超出这个大小,直接返回 null
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
                    }
                   	// 获取新的处理链
                    chain = SlotChainProvider.newSlotChain();
                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }

lookProcessChain() 方法首先通过 ResourceWrapper 对象查询是否存在其责任链(ResourceWrapper 包含了资源的名称、类型等信息)。然后检查 chainMap 的长度是否超出允许的最大长度 6000,如果超出则返回 null。最终通过 SlotChainProvider.newSlotChain() 方法获取一个新的插槽链。并将传入的资源的链条及信息存入 chainMap 中。 我们继续看看 SlotChainProvider.newSlotChain() 是如何获取新的插槽链的,该方法最终是调用了 DefaultSlotChainBuilder.build() 方法,来获取 ProcessorSlotChain

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

        // Note: the instances of ProcessorSlot should be different, since they are not stateless.
    	// 这里同样是使用了 ServiceLoader 的方式获取 ProcessorSlot 的所有实现类
        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        for (ProcessorSlot slot : sortedSlotList) {
            // 筛选出不是 AbstractLinkedProcessorSlot 子类的实现类
            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;
    }

其实 ProcessorSlotChain 对象就是一个 node,其拥有 addFirst()addLast() 两个方法。分别从头尾添加元素。

执行责任链

在获取到处理链之后,判断处理链是否为空,如果不为空的话,就进行最后一步:调用每一个 ProcessorSlot 实现类的 entry() 方法。

 所有的插槽类

  • NodeSelectorSlot

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

  • ClusterBuilderSlot

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

  • StatisticSlot

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

  • FlowSlot

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

  • AuthoritySlot

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

  • DegradeSlot

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

  • SystemSlot

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

总结

本篇非常简略的学习了 sentinel 是如何对资源的限流、黑白名单的数据的统计。sentinel 通过责任链的方式,非常直观的做到了对一个资源不同业务维度的统计及流控熔断等措施。


知识共享许可协议
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。