Sentinel源码(三)ProcessorSlot(上)

627 阅读5分钟

前言

Sentinel1.8总共有8个重要的Slot,都是通过SPI机制加载的。

slots.gif

本章学习前三个ProcessorSlot:

  1. NodeSelectorSlot:构建资源(Resource)的路径(DefaultNode),用树的结构存储。
  2. ClusterBuilderSlot:构建ClusterNode,用于记录资源维度的统计信息。
  3. StatisticSlot:使用Node记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑。

这三个Slot为规则校验提供数据支撑。

1、Node

在阅读NodeSelectorSlot和ClusterBuilderSlot源码之前,再深入学习一下Node。

Node的作用主要是为FlowRule流控规则,提供数据支撑

回顾第一章内容,在Sentinel中,Node的实现类有四个,StatisticNode负责统计指标数据,无论是DefaultNode还是ClusterNode都继承了StatisticNode。

Node.png

DefaultNode

DefaultNode,普通链路节点。

统计维度:Context + Resource。

创建时机:多次调用SphU.entry,NodeSelectorSlot执行时创建DefaultNode(如果Context+Resource维度已经有这个Node了,将使用原来的Node实例),并加入Context上下文的Node链表中。

public class DefaultNode extends StatisticNode {
    // 关联的资源
    private ResourceWrapper id;
    // 子节点
    private volatile Set<Node> childList = new HashSet<>();
    // 关联的ClusterNode
    private ClusterNode clusterNode;
}

DefaultNode维护了一个Node集合,用于表示当前节点的子节点,通过这种方式,DefaultNode构成了一颗树

DefaultNode重写了StaticNode更新统计数据的相关方法,如increaseBlockQps增加被拒绝的QPS时,额外调用了ClusterNode的increaseBlockQps方法。

@Override
public void increaseBlockQps(int count) {
    super.increaseBlockQps(count);
    this.clusterNode.increaseBlockQps(count);
}

EntranceNode

EntranceNode入口节点,是特殊的DefaultNode链路节点。

统计维度:Context。

创建时机:每个上下文Context,都会有一个EntranceNode与之关联,代表链路的入口。

EntranceNode继承了DefaultNode,是DefaultNode树的树根

EntranceNode重写了所有统计方法,比如avgRt统计平均响应时间,根据所有子节点的统计数据,计算得到最终的平均响应时间。

public class EntranceNode extends DefaultNode {
    public EntranceNode(ResourceWrapper id, ClusterNode clusterNode) {
        super(id, clusterNode);
    }
    @Override
    public double avgRt() {
        double total = 0;
        double totalQps = 0;
        for (Node node : getChildList()) {
            total += node.avgRt() * node.passQps();
            totalQps += node.passQps();
        }
        return total / (totalQps == 0 ? 1 : totalQps);
    }
}

ClusterNode

ClusterNode簇点,统计一个Resource的数据。

统计维度:Resource。

创建时机:多次调用Sph.entry获取不同资源,ClusterBuilderSlot执行时创建ClusterNode。如果这个Resource从来没有被entry调用过,则创建一个ClusterNode,否则沿用Resource对应的ClusterNode。

public class ClusterNode extends StatisticNode {
    // 资源名称
    private final String name;
    // 资源类型
    private final int resourceType;
    // origin来源 - 统计数据
    private Map<String, StatisticNode> originCountMap = new HashMap<>();
}

一个Resource对应一个ClusterNode,ClusterNode还根据Sph.entry时传入的origin不同(origin存储在上下文中),在originCountMap中保存了Resource下不同origin的统计数据StatisticNode。

区别

其实三者的主要区别在于统计维度的不同,由于统计维度不同,创建时机和数量就不同:

  1. DefaultNode:统计维度是Context + Resource,表示同一个上下文中的同一资源,共享一个DefaultNode实例,在NodeSelectorSlot中创建;
  2. EntranceNode:统计维度是Context,表示同一个上下文中,共享同一个EntranceNode实例,在Context创建时创建;
  3. ClusterNode:统计维度是Resource,表示同一个资源共享同一个ClusterNode实例,在ClusterBuilderSlot中创建;

此外,在数据结构上,DefaultNode是一棵树,EntranceNode继承了DefaultNode是树根,这样在每个Context中,EntranceNode+DefaultNode(s)构成了一整颗链路树。

通过这颗树的不同节点,可以得到每个Context+Resource维度下,当前节点的流量指标。

案例

从NodeSelectorSlot的javadoc中,找到了关于三个Node的关系的案例。

案例一

对于下面这段代码:

ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
     nodeA.exit();
}
ContextUtil.exit();

会生成这样的树:

Node树1.png

首先machine-root是一个Java进程中【真正】的树根,EntranceNode是每个Context的树根。

public final class Constants {
    public final static String ROOT_ID = "machine-root";

    /**
     * Global ROOT statistic node that represents the universal parent node.
     */
    public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
        new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));
}

ContextUtil.enter("entrance1", "appA")执行后,会创建名为entrance1的Context和他对应的EntranceNode;

SphU.entry("nodeA")执行后,NodeSelectorSlot会在EntranceNode下新增一个DefaultNode子节点nodeA;

又由于nodeA这个资源是首次调用entry方法,为了记录Resource维度的流量指标,ClusterBuilderSlot会创建资源nodeA对应的ClusterNode。

案例二

案例二通过开启多个Context,展现Node树的数据结构。

ContextUtil.enter("entrance1", "appA");
Entry nodeA = SphU.entry("nodeA");
if (nodeA != null) {
      nodeA.exit();
}
ContextUtil.exit();

ContextUtil.enter("entrance2", "appA");
nodeA = SphU.entry("nodeA");
if (nodeA != null) {
     nodeA.exit();
}
ContextUtil.exit();

Node树2.png

通过ContextUtil.enter,可以创建多个Context对应EntranceNode,统计Context维度流量;

通过SphU.entry,可以创建多个DefaultNode,统计Context+Resource维度流量;

同样的Resource,对应同一个ClusterNode实例,统计Resource维度流量。

2、NodeSelectorSlot

NodeSelectorSlot负责构造并存储DefaultNode,DefaultNode负责统计Context+Resource维度流量;

@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * {@link DefaultNode}s of the same resource in different context.
     * 一个Resource 对应一个NodeSelectorSlot实例 即对应一个这个map
     * map的key是Context,所以这个map存储的Node是Context+Resource维度的Node
     */
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
}

NodeSelectorSlot内部用一个Map存储context和DefaultNode的映射关系;

又因为,上一章讲到CtSph中存储了Resource与ProcessorSlotChain的映射关系,即一个Resource对应一个ProcessorSlotChain实例,一个ProcessorSlotChain实例只包含一个NodeSelectorSlot实例,所以NodeSelectorSlot里的Map是针对一个Resource的

// CtSph.java
/**
* Same resource({@link ResourceWrapper#equals(Object)}) will share the same
* {@link ProcessorSlotChain}, no matter in which {@link Context}.
*/
private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap = new HashMap<ResourceWrapper, ProcessorSlotChain>();
ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
  // 1. 同一个name的资源,会使用同一个ProcessorSlotChain
  ProcessorSlotChain chain = chainMap.get(resourceWrapper);
  if (chain == null) {
    //...
  }
  return chain;
}

综上NodeSelectorSlot实例里的map维护了Context+Resource维度对应的流量DefaultNode。

NodeSelectorSlot的entry方法主要分为两步:

  1. 获取Resource+Context维度的DefaultNode,如果不存在则创建
  2. 更新当前Context中的调用树
// NodeSelectorSlot.java
/**
* {@link DefaultNode}s of the same resource in different context.
* 一个Resource 对应一个NodeSelectorSlot实例 即对应一个这个map
* map的key是Context,所以这个map存储的Node是Context+Resource维度的Node
*/
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
  throws Throwable {
  DefaultNode node = map.get(context.getName());
  if (node == null) {
    synchronized (this) {
      node = map.get(context.getName());
      if (node == null) {
        // 1. 创建DefaultNode,存储到当前NodeSelectorSlot实例
        node = new DefaultNode(resourceWrapper, null);
        HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
        cacheMap.putAll(map);
        cacheMap.put(context.getName(), node);
        map = cacheMap;
        // 2. 将这个DefaultNode加入调用树
        ((DefaultNode) context.getLastNode()).addChild(node);
      }
    }
  }
  // 3. 设置当前节点
  context.setCurNode(node);
  // 4. 执行后面的Slot
  fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

值得注意的是,如果Context+Resource维度已经存在了对应DefaultNode,调用树的结构不会更新,只会设置当前节点为对应的DefaultNode。

比如:A.entry->B.entry->B.exit->A.exit->B.entry->B.exit

public static void m1() {
  Entry nodeA = SphU.entry("nodeA");
  Entry nodeB = SphU.entry("nodeB");
  nodeB.exit();
  nodeA.exit();
}
public static void m2() {
  Entry nodeB = SphU.entry("nodeB");
  nodeB.exit();
}
public static void main(String[] args) {
   m1();
   m2();
}

最后的结果还是树结构。

Node树3.png

如果要更精细地使用FlowRule流控规则,可以在入口先创建自定义上下文,设置FlowRule#strategy=STRATEGY_CHAIN(链路),这样可以对Resource+Context维度做流控,具体见后面FlowSlot的分析。

3、ClusterBuilderSlot

ClusterBuilderSlot构建ClusterNode,用于记录Resource维度的统计信息。

@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {
    // 保存全局 资源 - 资源统计数据
    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
    private static final Object lock = new Object();
    // ClusterBuilderSlot每个资源一个实例,这里保存当前资源对应ClusterNode
    private volatile ClusterNode clusterNode = null;
}

ClusterBuilderSlot维护了一个Map,保存所有Resource对应的ClusterNode;

另外,还是由于同一Resource对应一个ClusterBuilderSlot实例,clusterNode保存了当前资源对应的流量信息ClusterNode。

ClusterBuilderSlot的entry方法主要分为三步:

  1. 获取Resource对应ClusterNode,如果不存在则创建;
  2. 保存ClusterNode到当前上下文当前处理节点DefaultNode(NodeSelectorSlot传入)中;
  3. 如果当前上下文中设置了来源系统origin,则创建origin + Resource维度ClusterNode(一个单纯的StatisticNode,由对应ClusterNode维护) ,存入当前ClusterNode,设置origin ClusterNode到上下文中继续向后传递;
// ClusterBuilderSlot.java
// 保存全局 资源 - 资源统计数据
private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();
private static final Object lock = new Object();
// ClusterBuilderSlot每个资源一个实例,这里保存当前资源对应ClusterNode
private volatile ClusterNode clusterNode = null;

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args)
    throws Throwable {
    // 1. 构造Resource对应ClusterNode
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);
                clusterNodeMap = newMap;
            }
        }
    }
    // 2. 保存ClusterNode到当前上下文正在处理的DefaultNode
    node.setClusterNode(clusterNode);

    // 3. 设置origin ClusterNode
    if (!"".equals(context.getOrigin())) {
        Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
        context.getCurEntry().setOriginNode(originNode);
    }

    // 4. 执行下一个slot
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

4、StatisticSlot

StatisticSlot负责记录指标信息,如RT、Pass/Block Count,为后续规则校验提供数据支撑。

StatisticSlot是全局单例,entry方法分为两步:

  1. 先执行后续的规则校验Slot(fireEntry);
  2. 如果校验通过,统计threadNum/passRequest;如果发生PriorityWaitException异常,只统计threadNum;如果发生BlockException,统计blockQps;
@Spi(order = Constants.ORDER_STATISTIC_SLOT)
public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // 1. 先执行后续Slot的entry
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // 2. 统计数据ThreadNum++ passRequest++
            // 2-1. DefaultNode Resource+Context维度  ClusterNode Resource 维度
            node.increaseThreadNum();
            node.addPassRequest(count);

            // 2-2. origin ClusterNode  origin+Resource维度
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
                context.getCurEntry().getOriginNode().addPassRequest(count);
            }

            // 2-3 全局入口流量统计
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseThreadNum();
                Constants.ENTRY_NODE.addPassRequest(count);
            }

            // 3. 给用户的扩展点,可以通过SPI加载
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (PriorityWaitException ex) {
            // 这是流控规则才会抛出的异常
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseThreadNum();
            }
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onPass(context, resourceWrapper, node, count, args);
            }
        } catch (BlockException e) {
            // 1. 设置BlockError到上下文中
            context.getCurEntry().setBlockError(e);
            // 2. 统计数据 BlockQps++
            // 2-1. DefaultNode Resource+Context维度  ClusterNode Resource 维度
            node.increaseBlockQps(count);
            // 2-2 origin ClusterNode  origin+Resource维度
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseBlockQps(count);
            }
            // 2-3 全局入口流量统计
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                Constants.ENTRY_NODE.increaseBlockQps(count);
            }
            // 3. 给用户的扩展点,可以通过SPI加载
            for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
                handler.onBlocked(e, context, resourceWrapper, node, count, args);
            }

            throw e;
        } catch (Throwable e) {
            context.getCurEntry().setError(e);
            throw e;
        }
    }
}

无论是规则校验正常还是异常,都会从几个维度来统计流量数据:

  • DefaultNode:Resource+Context维度,注意这里的指标统计方法都被重写了,额外操作了ClusterNode;
  • ClusterNode:Resource维度;
// DefaultNode.java
@Override
public void increaseBlockQps(int count) {
  super.increaseBlockQps(count); // 增加DefaultNode自身blockQps
  this.clusterNode.increaseBlockQps(count); // 增加ClusterNode.blockQps
}
  • origin ClusterNode:如果上下文的来源系统非空,则记录origin+Resource维度流量;
  • 全局入口流量:如果资源属于入口流量,则记录到全局入口流量中,用于系统规则校验;

总结

本章主要学习了三个Slot:

  • NodeSelectorSlot,构建Node树,每个Node节点都是一个DefaultNode,记录Context+Resource维度数据;
  • ClusterBuilderSlot,针对Resource构建ClusterNode,每个ClusterNode维护Resource维度数据。如果当前上下文设置了来源系统origin,则在当前Resource维度下的ClusterNode内,创建origin+Resource维度的StatisticNode;
  • StatisticSlot:利用上述两个Slot构建的各种Node,执行数据统计。先执行后续规则校验Slot,如果校验通过,统计threadNum/passRequest;如果发生PriorityWaitException异常(流控规则,默认流控效果下,priority=true,才会出现,发生该异常会直接放行该请求),只统计threadNum;如果发生BlockException,统计blockQps。