Sentinel 实现原理—— Context

引言

在前面的文章中,我已经介绍了 Sentinel 的整体设计思想,本文主要介绍 Sentinel 中贯穿整个调用链路的 Context 容器实现,和 Sentinel 相关的所有文章均会收录于<Sentinel系列文章>中,感兴趣的同学可以看一下。

源码解读

Context 容器所存储的数据并不多,只包含如下属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// com.alibaba.csp.sentinel.context.Context
public class Context {

/**
* Context name.
*/
private final String name;

/**
* The entrance node of current invocation tree.
*/
private DefaultNode entranceNode;

/**
* Current processing entry.
*/
private Entry curEntry;

/**
* The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
*/
private String origin = "";

private final boolean async;
// ...
}

基本上 Context 实例在创建的时候,大部分属性就确定下来了,其中只有描述当前所处调用点的 curEntry 属性会伴随着调用链路的变化而变化。接下来,我们以同步模式的 Context 为例,简单地介绍一下 Context 的创建过程。下面的代码就是 ContextUtil::enter 接口的实现,从中可以看出 Context 本质上就是一个保存在 ThreadLocal 中的 POJO。每当执行 ContextUtil::enter 时,都会去 ThreadLocal 中检查是否已经生成了 Context,如果是的话就直接返回,否则就创建 Context 实例并保存在 ThreadLocal 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// com.alibaba.csp.sentinel.context.ContextUtil
/**
* Store the context in ThreadLocal for easy access.
*/
private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();

/**
* Holds all {@link EntranceNode}. Each {@link EntranceNode} is associated with a distinct context name.
*/
private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();

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

public static Context enter(String name, String origin) {
// 防止用户输入的 Context name 和默认 context name 冲突,创建默认 Context 时会走一个 internalEnter 函数,那个函数中没有下述检查
if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
throw new ContextNameDefineException(
"The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
}
return trueEnter(name, origin);
}

protected static Context trueEnter(String name, String origin) {
Context context = contextHolder.get();
if (context == null) {
Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
DefaultNode node = localCacheNameMap.get(name);
if (node == null) {
// MAX_CONTEXT_NAME_SIZE = 2000,防止 Context 过多,如果 Context 太多则跳过所有检查过程
if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
LOCK.lock();
try {
node = contextNameNodeMap.get(name);
if (node == null) {
if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
setNullContext();
return NULL_CONTEXT;
} else {
node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
// Add entrance node to machine-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 = new Context(node, name);
context.setOrigin(origin);
contextHolder.set(context);
}

return context;
}

在创建 Context 对象时,还涉及到了 EntranceNode 的初始化,从上述代码中可以看到 Sentinel 使用到了 Double-Check 来保证相同 Name 的 Context 只会映射到同一个 EntranceNode 实例。创建好 EntranceNode 后,不仅会将其保存在 contextNameNodeMap 中以备重复使用,还会将其挂载在根节点(machine-root)上,这也是整个调用树维护工作的起点,执行完这一步后,整个调用树会如下图所示。
entrance-node

上面就是 Context 中所有恒定属性的初始化过程,而 curEntry 属性会在每次产生新的调用点 Entry 时,动态的修改,同时创建调用点的过程也需要借助 Context 中保存的 curEntry 属性来维护 Entry 之间的父子关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// com.alibaba.csp.sentinel.CtEntry
// 每次创建 Entry 实例时都会修改 Context 中的 curEntry
CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
super(resourceWrapper);
this.chain = chain;
this.context = context;

setUpEntryFor(context);
}

private void setUpEntryFor(Context context) {
// The entry should not be associated to NullContext.
if (context instanceof NullContext) {
return;
}
// 维护 Entry 之间的关系
this.parent = context.getCurEntry();
if (parent != null) {
((CtEntry)parent).child = this;
}
// 修改 Context 中的 curEntry
context.setCurEntry(this);
}

这里大家可能会有疑问,ContextUtil::enter 接口并不是一个必须调用的接口,如果我们不调用它 Context 又是在哪里创建的呢?其实,在调用 SphO#entry 时,最终会调用到一个叫做 entryWithPriority 的函数,这个函数会从 ThreadLocal 中获取当前 Context,如果发现 Context 不存在就会去创建默认 Context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// com.alibaba.csp.sentinel.CtSph#entryWithPriority(com.alibaba.csp.sentinel.slotchain.ResourceWrapper, int, boolean, java.lang.Object...)
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
Context context = ContextUtil.getContext();
if (context instanceof NullContext) {
// The {@link NullContext} indicates that the amount of context has exceeded the threshold,
// so here init the entry only. No rule checking will be done.
return new CtEntry(resourceWrapper, null, context);
}

if (context == null) {
// Using default context. CONTEXT_DEFAULT_NAME = sentinel_default_context
context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
}
// ...
}

// com.alibaba.csp.sentinel.CtSph.InternalContextUtil#internalEnter(java.lang.String)
static Context internalEnter(String name) {
return trueEnter(name, "");
}

和 Context 数据修改相关的内容大概就这些,在后面的文章中,我们再介绍 Context 数据的使用过程。

参考内容

[1] Sentinel GitHub 仓库
[2] Sentinel 官方 Wiki
[3] Sentinel 1.6.0 网关流控新特性介绍
[4] Sentinel 微服务流控降级实践
[5] Sentinel 1.7.0 新特性展望
[6] Sentinel 为 Dubbo 服务保驾护航
[7] 在生产环境中使用 Sentinel
[8] Sentinel 与 Hystrix 的对比
[9] 大流量下的服务质量治理 Dubbo Sentinel初涉
[10] Alibaba Sentinel RESTful 接口流控处理优化
[11] 阿里 Sentinel 源码解析
[12] Sentinel 教程 by 逅弈
[13] Sentinel 专题文章 by 一滴水的坚持

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/middleware/sentinel/impl-context/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议,转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。