Sentinel调用链路
(1) 什么是调用链路
在复杂的分布式系统调用链路里,想对部分服务做限流,首先要知道整体的调用链路才能去定制化限流。
比如在电商系统里,首页、商详、购物车、下单 等都会调用商品服务,假设商品服务达到服务QPS极限了,这个时候要限流,但是要保证下单调商品不限流,其他的按照优先级限流,要怎么做?
(2) 为什么需要调用链路
在复杂业务场景,需要对调用系统按照优先级去处理
(3) 调用链路原理
假设要自己实现,可能通过加一个系统标识、系统优先级去区分。
(3.1) 样例
//
(4) 源码解析
1.N叉树初始化
2.N叉树新增子节点
3.N叉树查询
- Sentinel里的节点用
DefaultNode
表示 DefaultNode - 根节点放在
Constants
类里,public final static DefaultNode ROOT
- 所有的节点缓存在
NodeSelectorSlot
的Map<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主要作用是负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
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>
*/