一, Sentinel里的ProcessorSlot责任链

上一文里我们介绍了Sentinel里的七种ProcessorSlot,它们分成两类,一类进行数据统计,一类实现降级功能。Sentinel整体工作流程是使用责任链模式将所有的ProcessorSlot按照一定的顺序组成一个单向链表,辅助完成资源指标数据统计的 ProcessorSlot 必须在实现降级功能的 ProcessorSlot 的前面,原因很简单,降级功能需要依据资源的指标数据做判断,当然,如果某个 ProcessorSlot 不依赖指标数据实现降级功能,那这个 ProcessorSlot 的位置就没有约束。

实现将 ProcessorSlot 串成一个单向链表的是 ProcessorSlotChain,这个 ProcessorSlotChain 是由 SlotChainBuilder 构造的,默认 SlotChainBuilder 构造的 ProcessorSlotChain 注册的 ProcessorSlot 以及顺序如下代码所示。

public class DefaultSlotChainBuilder implements SlotChainBuilder {
    @Override
    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 AuthoritySlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());
        return chain;
    }
}

如果是自定义的ProcessorSlot可以通过AbstractLinkedProcessorSlot进行构建

public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
    // 当前节点的下一个节点
    private AbstractLinkedProcessorSlot<?> next = null;

    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        this.next = next;
    }  

    @Override
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        if (next != null) {
            T t = (T) obj; 
            // 调用下一个 ProcessorSlot 的 entry 方法
            next.entry(context,resourceWrapper,t,count,prioritized,args);
        }
    }

    @Override
    public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        if (next != null) {
            // 调用下一个 ProcessorSlot 的 exit 方法
            next.exit(context, resourceWrapper, count, args);
        }
    }
}

ProcessorSlot 接口的定义如下:

public interface ProcessorSlot<T> {
    // 入口方法
    void entry(Context context, ResourceWrapper resourceWrapper, T param, int count, boolean prioritized,Object... args) throws Throwable;
    // 调用下一个 ProcessorSlot#entry 方法
    void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized,Object... args) throws Throwable;
    // 出口方法
    void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
    // 调用下一个 ProcessorSlot#exit 方法
    void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args);
}

主要方法参数:

  • context:当前调用链路上下文。
  • resourceWrapper:资源 ID。
  • param:泛型参数,一般用于传递 DefaultNode。
  • count:Sentinel 将需要被保护的资源包装起来,这与锁的实现是一样的,需要先获取锁才能继续执行。而 count 则与并发编程 AQS 中 tryAcquire 方法的参数作用一样,count 表示申请占用共享资源的数量,只有申请到足够的共享资源才能继续执行。例如,线程池有 200 个线程,当前方法执行需要申请 3 个线程才能执行,那么 count 就是 3。count 的值一般为 1,当限流规则配置的限流阈值类型为 threads 时,表示需要申请一个线程,当限流规则配置的限流阈值类型为 qps 时,表示需要申请 1 令牌(假设使用令牌桶算法)。
  • prioritized:表示是否对请求进行优先级排序,SphU#entry 传递过来的值是 false。
  • args:调用方法传递的参数,用于实现热点参数限流。

二,Sentinel整体工作流程

image.png

下面是不借助 Sentinel 提供的适配器示例

ContextUtil.enter("上下文名称,例如:sentinel_spring_web_context");
Entry entry = null;
try {
     entry = SphU.entry("资源名称,例如:/rpc/openfein/demo", EntryType.IN (或者 EntryType.OUT));
     // 执行业务方法
       return doBusiness();
} catch (Exception e) {
     if (!(e instanceof BlockException)) {
          Tracer.trace(e);
     }
     throw e;
} finally {
     if (entry != null) {
         entry.exit(1);
     }
     ContextUtil.exit();
}

流程为5步:

  1. 调用 ContextUtil#enter 方法。负责为当前调用链路创建 Context,创建好后存入ThreadLocal,以及为 Conetxt 创建 EntranceNode
  2. 调用 SphU#entry 方法。这里整体链路里的核心调用,CtSph 负责为资源创建 ResourceWrapper 对象并为资源构造一个全局唯一的 ProcessorSlotChain、为资源创建 CtEntry 并将 CtEntry 赋值给当前调用链路的 Context.curEntry、最后调用 ProcessorSlotChain#entry 方法完成一次单向链表的 entry 方法调用。
  3. 如果抛出异常,且异常类型非 BlockException 异常,则调用 Tracer#trace 方法记录异常,为当前资源的 DefaultNode 自增异常数
  4. 调用 Entry#exit 方法;
  5. 调用 ContextUtil#exit 方法。

关于其中第2点再补充一下:
调用了entry方法后,先进入到FlowSlot的entry方法进行限流过滤:

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方法:

public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
        if (ruleProvider == null || resource == null) {
            return;
        }
        Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
            for (FlowRule rule : rules) {
                if (!canPassCheck(rule, context, node, count, prioritized)) {
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
}

这里拿到了我们设置的 FlowRule 循环匹配资源进行限流过滤。这就是Sentinel 能做到限流的原因。

参考文章:
06 Sentinel 中的责任链模式与 Sentinel 的整体工作流程
Sentinel限流实现原理


步履不停
38 声望13 粉丝

好走的都是下坡路