Sentinel责任链模式与工作流程:一次请求的完整旅行

图片[1]-Sentinel责任链模式与工作流程:一次请求的完整旅行 - 速优课-速优课

Sentinel责任链模式与工作流程:一次请求的完整旅行

前言

前面我们了解了Sentinel的核心概念和各种Node,今天我们来看看Sentinel的整体工作流程。

Sentinel的骨架是责任链模式——所有的ProcessorSlot按顺序串成一条链,请求依次经过每个Slot的处理。理解了这条链,就理解了Sentinel的核心骨架。

这篇文章,我们就从责任链模式讲起,再一步步拆解一次请求在Sentinel中的完整旅行。

一、什么是责任链模式

先简单复习一下责任链模式。

责任链模式,就是把多个处理器串成一条链,请求从链头进入,依次经过每个处理器,直到被处理完。每个处理器只负责自己那部分工作,处理完就传给下一个。

这个模式在很多框架里都有应用:

  • Shiro:用责任链实现权限过滤器链
  • Netty:用责任链把ChannelHandler串起来处理请求
  • Spring MVC:拦截器链也是类似的思想

Sentinel也用了责任链模式,不过是它自己的实现方式。

二、Sentinel的责任链结构

2.1 ProcessorSlot接口

先看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);
}

每个ProcessorSlot有两对方法:

  • entry:请求进入时调用(往链的后面传)
  • exit:请求退出时调用(往链的后面传)

注意:Sentinel的exit也是从前往后传的,不像Netty是从后往回传。这是两者的一个重要区别。

参数解析:

参数含义说明
context当前调用链路上下文贯穿整个调用链
resourceWrapper资源ID当前保护的资源
param泛型参数一般传递DefaultNode
count申请数量类似AQS的acquire参数,一般为1
prioritized是否优先级排序默认false
args调用参数用于热点参数限流

举个例子:DegradeSlot(熔断降级Slot)的工作方式就是——在entry方法中检查是否达到熔断阈值,如果是就抛出DegradeException,exit方法什么也不做。

2.2 责任链的默认顺序

Sentinel的ProcessorSlot可以分为两大类:

  • 辅助统计类:负责构建节点、统计数据
  • 降级功能类:负责实现各种降级功能

默认的Slot顺序是这样的:

NodeSelectorSlot → ClusterBuilderSlot → LogSlot → StatisticSlot → 
AuthoritySlot → SystemSlot → FlowSlot → DegradeSlot

为什么是这个顺序?

因为降级功能需要依赖统计数据,所以辅助统计的Slot必须放在前面。如果某个Slot不依赖统计数据,那位置就可以随意。

而且,同分类下的Slot也有严格顺序:

  • 辅助统计类:NodeSelectorSlot → ClusterBuilderSlot → StatisticSlot,顺序乱了会抛异常
  • 降级功能类:AuthoritySlot、SystemSlot、FlowSlot、DegradeSlot,顺序可以按需调整

2.3 默认的SlotChain构建器

DefaultSlotChainBuilder是默认的SlotChain构建器,代码很简单:

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;
    }
}

就是按顺序一个个addLast到链上。

三、责任链是怎么串起来的

3.1 AbstractLinkedProcessorSlot

为什么能串成链?因为所有ProcessorSlot都继承了AbstractLinkedProcessorSlot,这个类里有一个指向下一个节点的指针:

public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
    // 当前节点的下一个节点
    private AbstractLinkedProcessorSlot<?> next = null;
​
    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        this.next = next;
    }
}

然后,fireEntry和fireExit方法就是调用下一个节点的对应方法:

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

每个Slot的entry方法里,处理完自己的事情后,调用fireEntry把请求传给下一个Slot。exit同理。

3.2 DefaultProcessorSlotChain

ProcessorSlotChain是链的抽象,默认实现是DefaultProcessorSlotChain。

public class DefaultProcessorSlotChain extends ProcessorSlotChain {
    // 头节点(一个空实现的Slot,作为占位符)
    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;
}

first是一个空实现的Slot,作为链表的头节点(哨兵节点)。end指向链表的尾节点,方便addLast操作。

addFirst和addLast的实现也很直观:

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

调用链的entry和exit方法,就是从头节点开始:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, 
                  int count, boolean prioritized, Object... args) throws Throwable {
    first.entry(context, resourceWrapper, obj, count, prioritized, args);
}
​
@Override
public void exit(Context context, ResourceWrapper resourceWrapper, 
                 int count, Object... args) {
    first.exit(context, resourceWrapper, count, args);
}

3.3 每个资源一条链

Sentinel会为每个资源创建且仅创建一个ProcessorSlotChain,资源名称相同就用同一条链。

这些链被缓存在CtSph的chainMap里:

public class CtSph implements Sph {
    // 资源与ProcessorSlotChain的映射
    private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
        = new HashMap<ResourceWrapper, ProcessorSlotChain>();
}

在entryWithPriority方法中,会先查找对应的chain,找不到就创建:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, 
                                boolean prioritized, Object... args) throws BlockException {
    Context context = ContextUtil.getContext();
    // 查找或创建ProcessorSlotChain
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    // 创建CtEntry
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 调用链的entry方法
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    }
    return e;
}

为什么每个资源一条链?因为不同资源的规则配置不一样,统计数据也是分开的。每条链独立工作,互不干扰。

四、Sentinel的整体工作流程

说了这么多,一次请求在Sentinel中到底是怎么流动的?

我们先看一下不使用适配器的情况下,Sentinel的标准用法:

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

整个流程可以分为五步:

  1. ContextUtil.enter():创建调用上下文
  2. SphU.entry():进入资源保护(核心)
  3. Tracer.trace():记录业务异常(非BlockException)
  4. Entry.exit():退出资源保护
  5. ContextUtil.exit():销毁调用上下文

下面我们一步步拆解。

4.1 第一步:ContextUtil.enter()

ContextUtil.enter()负责为当前调用链路创建Context,以及创建EntranceNode。

核心逻辑:

protected static Context trueEnter(String name, String origin) {
    // 先从ThreadLocal里拿
    Context context = contextHolder.get();
    if (context == null) {
        // 从缓存中找EntranceNode
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 双重检查锁,创建EntranceNode
            LOCK.lock();
            try {
                node = contextNameNodeMap.get(name);
                if (node == null) {
                    node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                    // 添加到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
        context = new Context(node, name);
        context.setOrigin(origin);
        // 放入ThreadLocal
        contextHolder.set(context);
    }
    return context;
}

关键点:

  • ThreadLocal存储Context:每个线程一个Context
  • 每个Context名称对应一个EntranceNode:比如sentinel_spring_web_context这个名称,全局只有一个EntranceNode
  • EntranceNode挂在ROOT下面:形成全局的调用树

打个比方:

  • 每次请求都会创建一个新的Context(因为每个请求是一个线程)
  • 但这些Context的名称都是sentinel_spring_web_context
  • 所以它们共用同一个EntranceNode
  • 所有接口的DefaultNode都挂在这个EntranceNode下面

4.2 第二步:SphU.entry()

这是Sentinel最核心的一步。

SphU.entry() → CtSph.entry() → entryWithPriority(),整个过程:

  1. 创建ResourceWrapper(资源ID)
  2. 查找或创建ProcessorSlotChain(每个资源一条链)
  3. 创建CtEntry,并设置为Context.curEntry
  4. 调用ProcessorSlotChain.entry(),让请求经过整个责任链
  5. 如果抛出BlockException,说明被限流/熔断了,调用exit清理后抛出

ProcessorSlotChain的entry调用过程大概是这样的:

图片[2]-Sentinel责任链模式与工作流程:一次请求的完整旅行 - 速优课-速优课

请求依次经过:

  • NodeSelectorSlot:创建DefaultNode,构建调用链
  • ClusterBuilderSlot:创建ClusterNode
  • LogSlot:日志
  • StatisticSlot:统计数据(先调用后面的Slot,根据结果统计)
  • AuthoritySlot:黑白名单检查
  • SystemSlot:系统自适应限流
  • FlowSlot:限流检查
  • DegradeSlot:熔断检查

任何一个Slot抛出BlockException,流程就会中断。

4.3 第三步:Tracer.trace()

只有抛出非BlockException异常时,才会调用Tracer.trace()。

什么意思?BlockException是Sentinel自己抛出的(限流、熔断等),不算业务异常。只有业务代码抛出的异常,才需要记录到异常统计里。

private static void traceExceptionToNode(Throwable t, int count, Entry entry, DefaultNode curNode) {
    if (curNode == null) {
        return;
    }
    ClusterNode clusterNode = curNode.getClusterNode();
    if (clusterNode == null) {
        return;
    }
    // 记录异常
    clusterNode.trace(t, count);
}

ClusterNode的trace方法:

public void trace(Throwable throwable, int count) {
    if (count <= 0) {
        return;
    }
    if (!BlockException.isBlockException(throwable)) {
        // 非BlockException,才自增异常总数
        this.increaseExceptionQps(count);
    }
}

为什么是ClusterNode而不是DefaultNode?因为熔断降级、限流降级用的都是ClusterNode的全局统计数据。

4.4 第四步:Entry.exit()

请求处理完(不管成功还是失败),都要调用entry.exit()。

CtEntry的exit方法核心逻辑:

protected void exitForContext(Context context, int count, Object... args) {
    if (context != null) {
        // 1、调用ProcessorSlotChain的exit方法
        if (chain != null) {
            chain.exit(context, resourceWrapper, count, args);
        }
        // 2、还原Context.curEntry为父Entry
        context.setCurEntry(parent);
        if (parent != null) {
            ((CtEntry)parent).child = null;
        }
    }
}

两件事:

  1. 调用链的exit方法:让每个Slot做退出时的处理(比如StatisticSlot记录成功、减少线程数等)
  2. 还原Context.curEntry:把当前Entry从链表上摘下来,curEntry指回父Entry

还记得之前说的父子Entry双向链表吗?每次创建新的CtEntry,就把它设为Context.curEntry。exit的时候,再把curEntry还原为父Entry。就像栈一样,后进先出。

4.5 第五步:ContextUtil.exit()

最后一步,清理ThreadLocal。

public static void exit() {
    Context context = contextHolder.get();
    if (context != null && context.getCurEntry() == null) {
        contextHolder.set(null);
    }
}

只有当Context.curEntry为空时,才把Context从ThreadLocal中移除。

为什么要加这个判断?因为一次请求中可能多次调用SphU.entry(),每次entry对应一次exit。只有当所有entry都exit了(curEntry为空),才说明整个调用链结束了,才能清理Context。

五、整体流程总结

用一张流程图来总结一下:

请求到来
  │
  ▼
ContextUtil.enter() ──→ 创建Context,创建/获取EntranceNode
  │
  ▼
SphU.entry() ──→ 创建CtEntry,调用ProcessorSlotChain.entry()
  │
  ├─→ NodeSelectorSlot:创建DefaultNode
  ├─→ ClusterBuilderSlot:创建ClusterNode
  ├─→ LogSlot:日志
  ├─→ StatisticSlot:前置处理(线程数+1等)
  ├─→ AuthoritySlot:黑白名单检查
  ├─→ SystemSlot:系统自适应检查
  ├─→ FlowSlot:限流检查 ──→ 不通过 → 抛FlowException
  └─→ DegradeSlot:熔断检查 ──→ 不通过 → 抛DegradeException
  │
  ▼ (通过所有检查)
执行业务代码
  │
  ▼ (业务异常)
Tracer.trace() ──→ 记录异常(非BlockException)
  │
  ▼
Entry.exit() ──→ 调用ProcessorSlotChain.exit(),还原curEntry
  │
  ▼
ContextUtil.exit() ──→ 清理ThreadLocal

总结与思考

这篇文章我们从责任链模式讲起,拆解了Sentinel的整体工作流程。

几个关键要点再回顾一下:

  1. 责任链是Sentinel的骨架:所有功能都通过ProcessorSlot实现,串成一条链
  2. 每个资源一条链:不同资源的链是独立的,缓存在chainMap中
  3. Slot顺序有讲究:统计类在前,功能类在后;有些Slot顺序不能乱
  4. 一次请求五步走:enter → entry → 业务 → exit → exit
  5. Context通过ThreadLocal传递:每个请求一个Context,贯穿整个调用链
  6. 父子Entry形成栈结构:多次entry形成双向链表,exit时按顺序还原

思考一下:

  • 为什么Sentinel选择责任链模式?(扩展性好,每个Slot职责单一,可以灵活插拔)
  • 为什么每个资源一条链,而不是全局一条链?(不同资源规则不同,统计独立)
  • exit方法为什么也是从前往后传,而不是从后往回传?(设计选择,Sentinel的exit主要做一些收尾统计,顺序影响不大)

理解了整体工作流程,后面我们再深入每个Slot的具体实现时,就知道它处于什么位置、起到什么作用了。

下一篇,我们来聊聊Java SPI机制,以及Sentinel是怎么利用SPI实现灵活扩展的。

© 版权声明
THE END
喜欢就支持一下吧
点赞13
相关推荐
评论 抢沙发

请登录后发表评论

    请登录后查看评论内容

温馨提示:
1、本内容转载于网络,版权归原作者所有!
2、本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
3、本内容若侵犯到你的版权利益,请联系我们,会尽快给予删除处理!