Sentinel调用链路

(1) 什么是调用链路

在复杂的分布式系统调用链路里,想对部分服务做限流,首先要知道整体的调用链路才能去定制化限流。

比如在电商系统里,首页、商详、购物车、下单 等都会调用商品服务,假设商品服务达到服务QPS极限了,这个时候要限流,但是要保证下单调商品不限流,其他的按照优先级限流,要怎么做?

(2) 为什么需要调用链路

在复杂业务场景,需要对调用系统按照优先级去处理

(3) 调用链路原理

假设要自己实现,可能通过加一个系统标识、系统优先级去区分。

(3.1) 样例

// 

(4) 源码解析

1.N叉树初始化
2.N叉树新增子节点
3.N叉树查询

  1. Sentinel里的节点用DefaultNode表示 DefaultNode
  2. 根节点放在 Constants类里,public final static DefaultNode ROOT
  3. 所有的节点缓存在NodeSelectorSlotMap<String, DefaultNode> map里,注意 map 是 private volatile修饰的,也就是只有NodeSelectorSlot类可以访问到

(4.1) N叉树初始化

(4.1.1) N叉树根节点

N叉树的根节点在Constants类里,是一个公共静态变量,可以随时访问

package com.alibaba.csp.sentinel;

/**
 * Sentinel 的通用常量。
 */
public final class Constants {

    public final static String ROOT_ID = "machine-root";

    /**
     * 全局统计根节点,代表通用父节点。
     */
    public final static DefaultNode ROOT = new EntranceNode(new StringResourceWrapper(ROOT_ID, EntryType.IN),
        new ClusterNode(ROOT_ID, ResourceTypeConstants.COMMON));

}

(4.1.2) N叉树默认子节点

在调用CtSph::entryWithPriority方法时,会执行 Context context = ContextUtil.getContext(); 这行代码
在调用ContextUtil时,会触发 static{}代码块初始化,这块代码对应的就是N叉树初始化的代码。

public class ContextUtil {

    /**
     * 把上下文存储到线程本地变量方便获取 
     */
    private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();

    /**
     * 持有所有 EntranceNode 。  每个 EntranceNode 都与一个不同的上下文名称相关联。
     */
    private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();

    private static final ReentrantLock LOCK = new ReentrantLock();
    private static final Context NULL_CONTEXT = new NullContext();

    // ContextUtil被调用时自动执行static里的方法
    static {
        // 缓存默认上下文的入口节点。
        initDefaultContext();
    }

    private static void initDefaultContext() {
        // sentinel_default_context
        String defaultContextName = Constants.CONTEXT_DEFAULT_NAME;
        EntranceNode node = new EntranceNode(new StringResourceWrapper(defaultContextName, EntryType.IN), null);
        // N叉树根节点添加子节点  
        Constants.ROOT.addChild(node);
        contextNameNodeMap.put(defaultContextName, node);
    }

}

注意,static代码块里的方法只会执行一次

(4.2) N叉树新增子节点

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

源码:https://github.com/alibaba/Sentinel/blob/1.8.6/sentinel-core/src/main/java/com/alibaba/csp/sentinel/slots/nodeselector/NodeSelectorSlot.java#L136

package com.alibaba.csp.sentinel.slots.nodeselector;

/**
 * 这个类将尝试通过构建调用跟踪
 * 
 * 如果需要,添加一个新的DefaultNode作为上下文中的最后一个子节点。
 * 上下文的最后一个节点是上下文的当前节点或父节点。
 * 将自身设置为上下文当前节点。
 *
 * 它的工作原理如下
 * <pre>
 * ContextUtil.enter("entrance1", "appA");
 * Entry nodeA = SphU.entry("nodeA");
 * if (nodeA != null) {
 *     nodeA.exit();
 * }
 * ContextUtil.exit();
 * </pre>
 *
 * 上面的代码会在内存中生成如下调用结构: 
 *
 * <pre>
 *
 *              machine-root
 *                  /
 *                 /
 *           EntranceNode1 (entrance1,appA)
 *               /
 *              /
 *        DefaultNode(nodeA)- - - - - -> ClusterNode(nodeA);
 * </pre>
 *
 * 这里的EntranceNode表示由ContextUtil.enter("entrance1", "appA") 给出的“entrance1”。
 * 
 * 
 * DefaultNode(nodeA) 和 ClusterNode(nodeA) 都保存了“nodeA”的统计信息,它由 SphU.entry("nodeA")给出
 * 
 * ClusterNode 由 ResourceId 唯一标识;  
 * DefaultNode 由 资源ID 和 Context 标识。
 * 换句话说,一个资源 ID 将为每个不同的上下文生成多个 DefaultNode,但只会生成一个 ClusterNode。
 * 
 * 
 * 以下代码显示了两个不同上下文中的一个资源 ID
 * <pre>
 *    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();
 * </pre>
 *
 * 上面的代码会在内存中生成如下调用结构: 
 *
 * <pre>
 *
 *                  machine-root
 *                  /         \
 *                 /           \
 *         EntranceNode1   EntranceNode2
 *               /               \
 *              /                 \
 *      DefaultNode(nodeA)   DefaultNode(nodeA)
 *             |                    |
 *             +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
 * </pre>
 *
 * 正如我们所见,在两个上下文中为“nodeA”创建了两个DefaultNode,但只创建了一个ClusterNode。
 *
 * 我们还可以通过调用来检查这个结构: curl http://localhost:8719/tree?type=root}
 * 
 * @see EntranceNode
 * @see ContextUtil
 */
@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * DefaultNode s 相同资源在不同上下文中。 
     * {@link DefaultNode}s of the same resource in different context.
     *
     * map的key是上下文名称,对应 context.getName()
     */
    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 {
        /*
         * 有趣的是,我们使用上下文名称而不是资源名称作为映射键。
         * 
         * 请记住,相同的资源 ResourceWrapper#equals() 将在全局范围内共享相同的ProcessorSlotChain,无论在哪个上下文中。
         * 因此,如果代码进入#entry(Context, ResourceWrapper, DefaultNode, int, Object...),资源名称必须相同,但上下文名称可能不同。
         * 
         * 如果我们使用 SphU#entry(String resource) 在不同的上下文中输入同一个资源,
         * 使用上下文名称作为映射键可以区分同一个资源。
         * 在这种情况下,将为每个不同的上下文(不同的上下文名称)创建具有相同资源名称的多个 DefaultNode 。
         * 
         * 考虑另一个问题。一个资源可能有多个DefaultNode,那么获取同一个资源的总统计数据最快的方法是什么?
         * 答案是所有具有相同资源名称的DefaultNode 共享一个 ClusterNode。
         * 有关详细信息,请参阅 ClusterBuilderSlot 。
         * 
         */
        // 获取当前上下文的DefaultNode
        DefaultNode node = map.get(context.getName());
        // 单例模式-DCL
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    // 新建节点
                    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;
                    // 构建调用树  构建N叉树
                    // 在当前上下文中添加子节点  
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }

        // 更新上下文里的当前处理节点
        context.setCurNode(node);

        // 调用责任链的下一个功能插槽
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

}    
package com.alibaba.csp.sentinel.node;


public class DefaultNode extends StatisticNode {

    /**
     * 孩子节点集合
     */
    private volatile Set<Node> childList = new HashSet<>();

    /**
     * 添加孩子节点
     *
     * @param node valid child node
     */
    public void addChild(Node node) {
        if (node == null) {
            RecordLog.warn("Trying to add null child to node <{}>, ignored", id.getName());
            return;
        }
        // 单例-DCL
        if (!childList.contains(node)) {
            synchronized (this) {
                if (!childList.contains(node)) {
                    Set<Node> newSet = new HashSet<>(childList.size() + 1);
                    newSet.addAll(childList);
                    // 添加孩子节点
                    newSet.add(node);
                    // 更新孩子节点
                    childList = newSet;
                }
            }
            RecordLog.info("Add child <{}> to node <{}>", ((DefaultNode)node).id.getName(), id.getName());
        }
    }

}
/**
 * <pre>
 *
 *                  machine-root
 *                  /         \
 *                 /           \
 *         EntranceNode1   EntranceNode2
 *               /               \
 *              /                 \
 *      DefaultNode(nodeA)   DefaultNode(nodeA)
 *             |                    |
 *             +- - - - - - - - - - +- - - - - - -> ClusterNode(nodeA);
 * </pre>
 */