Sentinel 实现原理——Context

简介: 在前面的文章中,我已经介绍了 Sentinel 的整体设计思想,本文主要介绍 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)上,这也是整个调用树维护工作的起点,执行完这一步后,整个调用树会如下图所示。
entrance-node

上面就是 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 数据的使用过程。

文章说明

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

stun

版权声明: 本博客所有文章除特别声明外,均采用 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 一滴水的坚持

相关文章
|
3月前
|
负载均衡 算法 Java
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
40岁老架构师尼恩分享了关于SpringCloud核心组件的底层原理,特别是针对蚂蚁集团面试中常见的面试题进行了详细解析。内容涵盖了Nacos注册中心的AP/CP模式、Distro和Raft分布式协议、Sentinel的高可用组件、负载均衡组件的实现原理等。尼恩强调了系统化学习的重要性,推荐了《尼恩Java面试宝典PDF》等资料,帮助读者更好地准备面试,提高技术实力,最终实现“offer自由”。更多技术资料和指导,可关注公众号【技术自由圈】获取。
蚂蚁面试:Nacos、Sentinel了解吗?Springcloud 核心底层原理,你知道多少?
|
3月前
|
运维 监控 算法
聊一聊Sentinel背后的原理
本文介绍了Sentinel的核心原理,包括流量控制、熔断降级、系统负载保护、实时监控和统计、与多种微服务框架的集成能力以及扩展性,强调了Sentinel在保障分布式系统稳定性方面的重要性。
167 0
|
5月前
|
运维 监控 NoSQL
【Redis】哨兵(Sentinel)原理与实战全解~炒鸡简单啊
Redis 的哨兵模式(Sentinel)是一种用于实现高可用性的机制。它通过监控主节点和从节点,并在主节点故障时自动进行切换,确保集群持续提供服务。哨兵模式包括主节点、从节点和哨兵实例,具备监控、通知、自动故障转移等功能,能显著提高系统的稳定性和可靠性。本文详细介绍了哨兵模式的组成、功能、工作机制以及其优势和局限性,并提供了单实例的安装和配置步骤,包括系统优化、安装、配置、启停管理和性能监控等。此外,还介绍了如何配置主从复制和哨兵,确保在故障时能够自动切换并恢复服务。
|
7月前
|
监控 Java 应用服务中间件
Sentinel原理及实践
Sentinel原理及实践
130 1
|
8月前
|
监控 NoSQL 程序员
Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理
Redis 高可用篇:你管这叫 Sentinel 哨兵集群原理
140 5
|
8月前
|
监控 算法 Java
sentinel 服务限流工作原理
sentinel 服务限流工作原理
|
8月前
|
监控 BI Sentinel
深入理解Sentinel系列-2.Sentinel原理及核心源码分析(下)
深入理解Sentinel系列-2.Sentinel原理及核心源码分析
138 0
|
8月前
|
存储 监控 测试技术
深入理解Sentinel系列-2.Sentinel原理及核心源码分析(上)
深入理解Sentinel系列-2.Sentinel原理及核心源码分析
532 0
|
算法 Java BI
Sentinel为什么这么强,我忍不住扒了扒背后的实现原理
大家好,我是三友~~ 最近我在整理代码仓库的时候突然发现了被尘封了接近两年之久的Sentinel源码库 两年前我出于好奇心扒了一下Sentinel的源码,但是由于Sentinel本身源码并不复杂,在简单扒了扒之后几乎就再没扒过了 那么既然现在又让我看到了,所以我准备再来好好地扒一扒,然后顺带写篇文章来总结一下。
Sentinel为什么这么强,我忍不住扒了扒背后的实现原理
|
算法 Java Sentinel
sentinel架构底层原理剖析详解
sentinel架构底层原理剖析详解
206 0