Sentinel 组件 (一)

本贴最后更新于 1611 天前,其中的信息可能已经物是人非

1. 介绍

当前许多项目承载着大量业务功能,在单一工程中进行开发会造成代码量剧增,为项目的开发、部署、运维以及扩展造成障碍。在此背景下,微服务的出现有利于解决上述出现的问题,然而服务的稳定性又造成巨大的困扰。

本章介绍的阿里 Sentinel 组件以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。该组件的核心功能不依赖任何外部项目,同时官方称引入 Sentinel 带来的性能损耗非常小,只有在业务单机量级超过 25W QPS 的时候才会有一些显著的影响(10% 左右),单机 QPS 不太大的时候损耗几乎可以忽略不计。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
  • 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

更多项目的介绍请移步官方文档。

2. 基础

Sentinel 组件以资源为单位,进行数据统计,为控制提供基础数据。每个资源都有一个资源名称,在一次调用中会涉及到以下概念:

  • slot:功能插槽。每个 slot 负责不同的职责,比如统计 slot 负责当前系统的数据统计,限流 slot 负责检查限流规则是否成立。
  • slot chain:功能插槽链。每一个资源都会涉及到不同的检查规则,所以在 Sentinel 中每个资源都拥有单独的 slot chain,保证资源和 slot chain 一一对应。在请求资源 X 时,只需要执行资源 X 对应的 slot chain 即可。
  • Node:统计节点,包含资源的全局数据统计,单次链路调用的统计等多个节点。
  • Entry:每一次资源的调用都会创建一个 Entry,它保存了本次的调用信息,如创建时间、当前的统计节点等。一个 Entry 负责一次 slot chain 的执行。
  • Context:调用链路上下文,通过 ThreadLocal 维持。在一次处理中,可能涉及到对一个资源的多次请求(可以理解为一个线程中请求获取两次 Entry),例如用户获取文章的信息,会涉及到获取文章的阅读量和评论数,这些数据都保存在另外一个服务 S 中,那么可能就会调用两次资源 S,所以创建两个 Entry,但是只维护一个 Context。

3. 执行流程

在项目中使用 Sentinel,最基础的使用方式如下:

public static void main(String[] args) {
    // 不断进行资源调用.
    while (true) {
        Entry entry = null;
        try {
	    entry = SphU.entry("HelloWorld");
            // 资源中的逻辑.
            System.out.println("hello world");
	} catch (BlockException e1) {
	    System.out.println("blocked!");
	} finally {
	   if (entry != null) {
	       entry.exit();
	   }
	}
    }
}

我们通过跟踪 entry = SphU.entry("HelloWorld"); 语句的执行,来看看 Sentinel 的执行流程。在此流程中,关键的代码是 CtSph 获取 Entry 的方法。

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 = 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;
    }

3.1. 规则开关

通过设置 Constants.ON 的值,可以控制是否进行规则检查。

if (!Constants.ON) { 
    return new  CtEntry(resourceWrapper, null, context); 
}

3.2. 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);
}

该部分获取上面提到的 Conext 对象,首先关注 ContextUtil 类:

private static ThreadLocal<Context> contextHolder = new ThreadLocal<>();

public static Context getContext() {
        return contextHolder.get();
    }

可以看见 Sentinel 为每一个线程实例化了一个 Context 对象,通过 ThreadLocal 进行保存,通常一次用户请求在一个线程中完成,所以保证了一个线程拥有一份唯一的上下文。
一般在线程第一次获取上下文时,此方法返回为 null,此时需要实例化,实例化方法如下:

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;
                } else {
                    try {
                        LOCK.lock();
                        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.
                                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;
    }

由代码可知,Sentinel 会创建一个 Context 放入到 contextHolder 中,下面关注下 contextNameNodeMap。该变量的申明如下:

private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();

此变量以 Context 的 Name 为 key,保存了 EntranceNode 对象。在组件中,资源的调用路径以树状结构存储起来,用于根据调用路径进行流量控制。在默认情况下,一次调用会形成以下结构:

             machine-root
                 /   
                /
         EntranceNode(sentinel_default_context)
              /
             /   
      DefaultNode(nodeA)

上面 EntranceNode 是由上述代码生成的。注意在生成 EntranceNode 时,如果超过 MAX_CONTEXT_NAME_SIZE,就会返回 NullContext 类型的 context。如果在一次调用中使用以下方法生成不同的 ContextName,那么就会形成下述结构。

 ContextUtil.enter("entrance1", "appA");
  Entry nodeA = SphU.entry("nodeA");
  if (nodeA != null) {
    nodeA.exit();
  }
  ContextUtil.exit();

  ContextUtil.enter("entrance2", "appA");
  nodeA = SphU.entry("nodeA");
  if (nodeA != null) {
    nodeA.exit();
  }
  ContextUtil.exit();
                machine-root
                   /         \
                  /           \
          EntranceNode1   EntranceNode2
                /               \
               /                 \
       DefaultNode(nodeA)   DefaultNode(nodeA)

详细介绍见官方文档

3.3. slot chain

 ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

上述代码用于获取此次执行的 slot chain,即一系列数据统计和规则检查的 slot。
通过 lookProcessChain 方法,我们可以获取以下信息:

  1. 上面提到每一个资源都有一份 slot chain,该信息以资源的名称为 key,保存于 Ctsph::chainMap 中。
  2. 一个项目最多只能存在 Constants.MAX_SLOT_CHAIN_SIZE(默认 6000)个资源,超过后不再生效。
  3. 通过 SlotChainProvider::newSlotChain 方法获取 slot chain,默认情况下会使用 **DefaultSlotChainBuilder ** 构造 slot chain,可以通过 SPI 进行单独的配置(SPI,即 Service Provider Interface,具体详情可以自行搜索,后续文章会进行简单介绍)。

在获取 slot chain 后,构造出 Entry 对象,然后通过以下方法调用 slot chain,依次执行每个 slot,基本流程结束。

chain.entry(context, resourceWrapper, null, count, prioritized, args);

DefaultSlotChainBuilder 中,通过以下方法返回 slot chain,其中的每个 slot 将会在以后的文章中详细介绍。

4. 后记

本文简单介绍了 Sentinel,并讲述了基本的执行流程。在此基础上,后面会跟随源码学习 Sentinel 的不同 slot。

1 操作
AlanSune 在 2020-06-07 21:16:28 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...