Sentinel上下文创建及执行

作者: 小白先生哦

Sentinel上下文创建及执行
Sentinel上下文创建及执行,入口示例代码:

public static void fun() {
    Entry entry = null;
    try {
        entry = SphU.entry(SOURCE_KEY);
    } catch (BlockException e1) {
        if (entry != null) {
            entry.exit();
        }
    }
}

执行entry

在执行SphU.entry时获取Entry,Entry代表当前调用的入口,用来保存当前调用信息。

进入到SphU.entry方法可以发现,Entry的获取使用的是Sph的默认实现CtSph。Sph是资源统计和规则检查的接口定义。

public class Env {
    public static final Sph sph = new CtSph();
    static {
        // If init fails, the process will exit.
        InitExecutor.doInit();
    }
}

进到CtSph.entry方法:

@Override
public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
    StringResourceWrapper resource = new StringResourceWrapper(name, type);
    return entry(resource, count, args);
}

可以看出第一步是创建一个当前资源的包装类,然后将标识当前请求资源的包装类传进entry方法获取Entry。值得一提的是StringResourceWrapper继承自ResourceWrapper。而ResourceWrapper重新了hashCode和equals方法,如下:

@Override
public int hashCode() {
    return getName().hashCode();
}
@Override
public boolean equals(Object obj) {
    if (obj instanceof ResourceWrapper) {
        ResourceWrapper rw = (ResourceWrapper)obj;
        return rw.getName().equals(getName());
    }
    return false;
}

可以看出比较两个Warpper是否指向同一个资源,主要是比较的name,只要获取的资源名相同那么就是要求获取同一个资源,这一点在后面有用。

然后回到CtSph.entry方法,最终进入到了CtSph.entryWithPriority方法,代码如下:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 1.从当前线程获取context
    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 = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }
    // Global switch is close, no rule checking will do.
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    /*
     * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
     * so no rule checking will be done.
     */
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

该方法做了如下几件事:

  1. 首先尝试从当前线程获取context,可以看ContextUtil.getContext方法:
    public static Context getContext() {
       return contextHolder.get();
    }
    
    查看contextHolder属性是一个ThreadLocal:
    /**
    * Store the context in ThreadLocal for easy access.
    */
    private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();
    
  2. 判断当前线程上下文是否超出了阈值,也就是下面语句:
    if (context instanceof NullContext)
    
    们可以看看NullContext的定义:
    /**
    * If total  exceed {@link ConstantsMAX_CONTEXT_NAME_SIZE}, a
    * {@link NullContext} will get when invoke {@link ContextUtil}.enter(), means
    * no rules checking will do.
    *
    * @author qinan.qn
    */
    public class NullContext extends Context {
       public NullContext() {
        super(null, "null_context_internal");
       }
    }
    
    当上面判断为真时,那么就不再进行规则检查。
  3. 当从当前线程获取的Context为空时,创建新的Context。**(这里在下面再详细解读。) **
  4. 获取当前资源对应的Slot执行链:
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    
    在获取执行链的方法:CtSph.lookProcessChain:
    ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
       ProcessorSlotChain chain = chainMap.get(resourceWrapper);
       if (chain == null) {
           synchronized (LOCK) {
               chain = chainMap.get(resourceWrapper);
               if (chain == null) {
                   // Entry size limit.
                   if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                       return null;
                   }
                   chain = SlotChainProvider.newSlotChain();
                   Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                       chainMap.size() + 1);
                   newMap.putAll(chainMap);
                   newMap.put(resourceWrapper, chain);
                   chainMap = newMap;
               }
           }
       }
       return chain;
    }
    
    其中chainMap定义:
    private static volatile Map<ResourceWrapper, ProcessorSlotChain> chainMap
           = new HashMap<ResourceWrapper, ProcessorSlotChain>();
    
    可以看出每个资源都是对应的一个执行链,在chainMap中就是用ResourceWrapper做为键类型,而们上面已经看到了ResourceWrapper重写了hashCode和equals方法,所以唯一确定一个资源的就是资源名。
  5. 执行Slot链,如果规则检查未通过那么抛出BlockException异常,否则代表符合规则进入成功。

然后接下来看一下Context的创建,也就是下面这段代码:

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

跟踪代码,最终进入到了ContextUtil.trueEnter方法。在阅读ContextUtil.trueEnte方法时,有必要先看一张图,来理清一下线程thread和Context、Context和Node之间的关系:

图片来源(该文章可以一看):https://www.jianshu.com/p/e39ac47cd893 img

前面代表的是3个线程,可以看成他们都是获取helloWorld资源,可以看出每一个线程在执行的时候都是独立的创建了一个Context,每一个线程里面的Context都是对应到了一个点EntranceNode上,而该EntranceNode则是用于存储一个资源的信息。梳理了这三者之间的关系,那么接下来看ContextUtil.trueEnt方法,代码如下:

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) {
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
                LOCK.lock();
                try {
                    node = contextNameNodeMap.get(name);
                    if (node == null) {
                        if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                            setNullContext();
                            return NULL_CONTEXT;
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // Add entrance node.
                            Constants.ROOT.addChild(node);
                            Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                            newMap.putAll(contextNameNodeMap);
                            newMap.put(name, node);
                            contextNameNodeMap = newMap;
                        }
                    }
                    LOCK.unlock();
                }
            }
        }
        context = new Context(node, name);
        context.setOrigin(origin);
        contextHolder.set(context);
    }
    return context;
}

这个方法做的事情如下:

  1. 从contextHolder中获取Context,如果有了那么就直接返回。
  2. 没有Context信息,那么准备开始创建该上下文信息,准备工作:从contextNameNodeMap中获取对应节点,contextNameNodeMap定义如下:
    private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();
    
    这个map中是上下文名和节点的对应关系,而上下文名即是资源名。
  3. 如果获取到了这个节点,那么直接创建一个Context并设置到contextHolder中,然后直接返回。
  4. 当上面节点不存在,那么先创建该节点,逻辑如下:
  5. 先检查当前上下文数是否超过指定阈值,如果超过了那么返回NullContext,本次请求不做规则检查。
  6. 没有超过指定阈值,那么加锁,进行双重检查。
  7. 使用当前资源创建节点,将创建的节点关联到根节点下,然后存入contextNameNodeMap中。然后创建Context并返回。可以看看在新增节点的时候,它的做法是在原map的基础上新建一个size+1的新map,然后将原map的所有节点信息加入新map中,同时保存新节点信息:
      Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
      newMap.putAll(contextNameNodeMap);
      newMap.put(name, node);
      contextNameNodeMap = newMap;
    
    而们的contextNameNodeMap属性是用volatile进行修饰的,当contextNameNodeMap引用的值发生变更时,能够立即对其它线程可见。 那么为什么不在原来的contextNameNodeMap中直接加入新节点,而要新建map然后进行一次复制呢?

做完上面这些事情后,们就得到了需要的Context。

执行exit

不管是上面示例代码中的finally里面的entry.exit()调用,还是CtSph.entryWithPriority方法中调用的e.exit(count, args)方法,最终都是在CtEntry.exitForContext方法中执行,代码如下:

protected void exitForContext(Context context, int count, Object... args) throws ErrorEntryFreeException {
    if (context != null) {
        // Null context should exit without clean-up.
        if (context instanceof NullContext) {
            return;
        }
        if (context.getCurEntry() != this) {
            String curEntryNameInContext = context.getCurEntry() == null ? null
                : context.getCurEntry().getResourceWrapper().getName();
            // Clean previous call stack.
            CtEntry e = (CtEntry) context.getCurEntry();
            while (e != null) {
                e.exit(count, args);
                e = (CtEntry) e.parent;
            }
            String errorMessage = String.format("The order of entry exit can't be paired with the order of entry"
                    + ", current entry in context: <%s>, but expected: <%s>", curEntryNameInContext,
                resourceWrapper.getName());
            throw new ErrorEntryFreeException(errorMessage);
            // Go through the onExit hook of all slots.
            if (chain != null) {
                chain.exit(context, resourceWrapper, count, args);
            }
            // Go through the existing terminate handlers (associated to this invocation).
            callExitHandlersAndCleanUp(context);
            // Restore the call stack.
            context.setCurEntry(parent);
            if (parent != null) {
                ((CtEntry) parent).child = null;
            }
            if (parent == null) {
                // Default context (auto entered) will be exited automatically.
                if (ContextUtil.isDefaultContext(context)) {
                    ContextUtil.exit();
                }
            }
            // Clean the reference of context in current entry to avoid duplicate exit.
            clearEntryContext();
        }
    }
}

逻辑比较简单,执行chain.exit,清空context等。

原文创作:小白先生哦

更多推荐

更多
  • 硅谷银行破产波及中国创业者 硅谷银行(Silicon Valley ...
  • 硅谷银行,一夜破产!储户疯狂挤兑,会影响科技行业吗? ...
  • Sentinel Golang使用-流量控制 流控规则,一条流控规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:Resource:资源名,即规则的作用目标,MetricType: 指标类型,Count: 流控阈值,RelationStrategy: ...
  • Sentinel Golang使用-新手指南 欢迎来到 Sentinel 的世界!这篇新手指南将指引您快速入门 Sentinel。资源埋点使用 Sentinel 的 Entry API ...
  • Sentinel Golang使用-实时监控 历史资源数据,秒级监控日志,资源的秒级日志(metric 日志)位于 ~/logs/csp/ 目录下,文件名格式为 ${appName}-metrics.log.Dec 10, 2023 9:32:14 AM.xx。例如,该日志的名字可能为...
  • Sentinel Golang使用-系统自适应流控 Sentin口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 系统保护规则是应用整体维度的,而不是单个调用维度的,并且仅对入口流量生效。目前系统规则支持以下的模式:Load 自适应 (仅对 ...
  • Sentinel Golang使用-通用配置项 若调用初始化函数时未指定配置路径,Sentinel 会尝试从环境变量 SENTINEL_CONFIG_FILE_PATH 中读取配置路径;若不存在,则 Sentinel 会尝试读取项目目录下 sentinel.yml 文件;若仍不存在,则 ...
  • Sentinel Golang使用-日志 默认的日志目录为 ~/logs/csp 目录。日志目录可在初始化日志时进行指定。秒级监控日志,请参考 [metric 日志文档]。record 日志,其它的日志位于 ${user_home}/logs/csp/sentinel-...
  • Sentinel Golang使用-如何使用 使用 Sentinel 主要分为以下几步:对 Sentinel 进行相关配置并进行初始化,埋点(定义资源),配置规则,通用配置及初始化,使用 Sentinel 时需要在应用启动时对 Sentinel 进行相关配置并触发初始化。api ...
  • Sentinel Golang使用-介绍 Sentinel 的主要工作机制如下:对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel ...
  • 近期文章

    更多
    文章目录

      推荐作者

      更多