Sentinel 实现原理——Context

引言

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

源码解读

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

// 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 中。

// 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)上,这也是整个调用树维护工作的起点,执行完这一步后,整个调用树会如下图所示。
Sentinel 实现原理——Context

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

// 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。

// 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 数据的使用过程。

文章说明

更多有价值的文章均收录于贝贝猫的文章目录

Sentinel 实现原理——Context

版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。

参考内容

[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 一滴水的坚持

上一篇:C#微信公众号开发--网页授权(oauth2.0)获取用户基本信息二


下一篇:Sentinel 定义资源