Sentinel 实现原理——Context

引言

在前面的文章中,我已经介绍了 Sentinel 的整体设计思想,本文主要介绍 Sentinel 中贯穿整个调用链路的 Context 容器实现。更多相关文章和其他文章均收录于贝贝猫的文章目录

源码解读

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

参考内容

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

stun

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Sentinel 是一款由阿里巴巴开源的分布式系统流量防卫兵。它的主要目标是通过实时的流量控制、熔断降级和系统负载保护等手段,来保证系统的稳定性和可靠性。 Sentinel实现原理主要包括以下几个方面: 1. 熔断降级:Sentinel 通过定义规则,监控系统中的资源(如接口、方法),当资源达到设定的阈值时,可以进行熔断,即临时关闭该资源的访问,避免故障扩大化。熔断后,Sentinel 会定时尝试恢复资源的访问,并根据具体情况逐渐放量。 2. 流量控制:Sentinel 可以对系统中的资源进行实时流量控制。通过定义规则,可以限制资源的最大访问量或并发数,防止系统被过高的请求压垮。 3. 系统负载保护:Sentinel 可以根据系统当前的负载情况,动态地调整流量控制策略。当系统负载过高时,可以通过限制资源的最大访问量来保护系统不被过载。 4. 实时监控和统计:Sentinel 提供了实时监控和统计功能,可以及时地了解系统中各个资源的运行情况和负载情况。通过监控数据,可以进行性能优化和故障排查。 相关问题: 1. 如何在项目中集成 Sentinel? 2. Sentinel 如何实现对资源的实时监控和统计? 3. Sentinel 的熔断降级策略是如何工作的? 4. 如何定义 Sentinel 的流量控制规则? 5. Sentinel 如何保证对系统的负载保护不会影响正常的业务流程?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝克街的流浪猫

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值