Alibaba Sentinel 骨架源码分析

Sentinel采用责任链模式,通过不同的Slot实现限流、降级和系统保护等功能。关键组件包括NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot等,共同构建灵活且可扩展的微服务保护框架。

Sentinel 的核心骨架,将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。核心结构:

image.png

业务埋点示例

// 资源的唯一标识
String resourceName = "testSentinel";
Entry entry = null;
String retVal;
try {
    entry = SphU.entry(resourceName, EntryType.IN);
    // TODO 业务逻辑 
    retVal = "passed";
} catch (BlockException e) {
    // TODO 降级逻辑
    retVal = "blocked";
} catch (Exception e) {
    // 异常数统计埋点
    Tracer.trace(e);
    throw new RuntimeException(e);
} finally {
    if (entry != null) {
        entry.exit();
    }
}

这段代码是Sentinel业务埋点示例,通过示例我们可以看出Sentinel对资源的控制入口是SphU.entry(resourceName, EntryType.IN);,源码如下:

public static Entry entry(String name, EntryType type) throws BlockException {
    return Env.sph.entry(name, type, 1, OBJECTS0);
}

这里第一个参数是受保护资源的唯一名称;第二个参数表示流量类型:

  • EntryType.IN:是指进入我们系统的入口流量,比如 http 请求或者是其他的 rpc 之类的请求,设置为IN主要是为了保护自己系统。
  • EntryType.OUT:是指我们系统调用其他第三方服务的出口流量,设置为OUT是为了保护第三方系统。

这段代码没什么逻辑,只是转发了下,跟进源码可以发现最终逻辑实在CtSph#entryWithPriority(ResourceWrapper, int, boolean, Object...)方法中。

Sentinel 骨架代码

Sentinel的核心是资源,这里的资源可以是任何东西,服务,服务里的方法,甚至是一段代码。而SphU.entry(resourceName);这段代码的主要作用是 :

  1. 定义一个Sentinel资源
  2. 检验资源所对应的规则是否生效

核心代码如下:

private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 获取当前线程上下文,Context是通过ThreadLocal维护,每一个Context都会有一个EntranceNode实例,它是dashboard【簇点链路】中的根节点,主要是用来区分调用链路的
    Context context = ContextUtil.getContext();
    if (context instanceof NullContext) {
        // 如果是 NullContext,表示 Context 个数超过了阈值,这个时候 Sentinel 不会应用规则,即不会触发限流降级等规则,也不会触发QPS等数据统计。
        // 阈值大小 =Constants.MAX_CONTEXT_NAME_SIZE = 2000,具体可以查看 ContextUtil#trueEnter。
        return new CtEntry(resourceWrapper, null, context);
    }

    if (context == null) {
        // 如果没有设置上下文,即使用默认上下文,默认上下文的名称是  sentinel_default_context
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }

    if (!Constants.ON) {
        // Sentinel 的全局控制开关,一旦关闭则不进行任何检查
        return new CtEntry(resourceWrapper, null, context);
    }

    // 通过Sentinel的官方文档我们可以知道,Sentinel的核心功能是基于一系列的功能插槽来实现的,而组织这些功能插槽使用的是责任链模式。
    // 这里是通过资源(每个资源是唯一的),获取第一个功能插,即该资源对应的规则入口。
    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.
     */
    // 如果一个服务中,资源数量操过阈值(最大的插槽链),则返回null,即不会再应用规则,直接返回。
    // 阈值大小 = Constants.MAX_SLOT_CHAIN_SIZE = 6000
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }

    // 构建Sentinel调用链入口
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // 开始执行插槽链,如果某个插槽匹配上了某个规则,如限流规则,就会抛出BlockException异常,这时表示请求被拒绝了。
        // 业务层面会去捕获这个异常,然后做熔断,降级操作。
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // Sentinel内部异常
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

核心逻辑如下:

  1. 通过当前线程的上下文,获取到当前线程的【簇点链路】入口。
  2. 判断全局开关是否关闭。
  3. 通过唯一的资源标识获取到对应的功能插槽链(ProcessorSlot)的第一个插槽。
  4. 构建Sentinel调用链入口,并执行调用链
  5. 如果抛出BlockException表示触发了资源限制规则,需要进行熔断降级。

这里有两个需要注意的地方:

  1. 【簇点链路】入口Context的数量是有限制的,最大2000个,通常情况下,我们都不需要显示设置 context,使用默认的就好了,这样Context数量限制基本上不会触发。
  2. SphU.entry(resourceName, EntryType.IN),这里的资源的唯一标识resourceName也是有限制的,最大是6000。当Sentinel与 Servlet 的整合后,CommonFilter会将所有的对外接口定义成Sentinel的资源,资源名称就是接口地址,所以要控制好服务接口数量。

image.png

ContextUtil#enter

ContextUtil#enter(String name, String origin)的主要作用就是创建当前线程的上下文Context,每个上下文会对应一个EntranceNode(入口节点)实例,通常情况下我们不需要显示调用该方法。

  • name:上下文的唯一标识,也是入口节点的资源名称。
  • orgin:表示来源,通常是服务消费者或调用者的应用名称,当我们需要对不同来源的消费者或调用者进行限制时就会用到这个参数。

源码如下:

public static Context enter(String name, String origin) {
    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) {
    // 通过ThreadLocal获取当前线程的上下文
    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);
                            // 将入口节点添加到全局根节点下(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;
}

如果我们再代码中显示调用这个方法:

ContextUtil.enter("context1", "service-1"); 
...
ContextUtil.exit();

ContextUtil.enter("context2", "service-1"); 
...
ContextUtil.exit();

那么会创建如下一个树结构图:

image.png

这里有两点需要注意:

  1. 也就是上面说的数量限制,2000。
  2. ContextUtil是通过ThreadLocal来维护当前线程的上下文的,所以当遇到异步线程时需要手动调用ContextUtil.runOnContext(context, f)方法来完成父线程和子线程的上下文切换。

文档中的Demo:

public void someAsync() {
    try {
        AsyncEntry entry = SphU.asyncEntry(resourceName);

        // Asynchronous invocation.
        doAsync(userId, result -> {
            // 在异步回调中进行上下文变换,通过 AsyncEntry 的 getAsyncContext 方法获取异步 Context
            ContextUtil.runOnContext(entry.getAsyncContext(), () -> {
                try {
                    // 此处嵌套正常的资源调用.
                    handleResult(result);
                } finally {
                    entry.exit();
                }
            });
        });
    } catch (BlockException ex) {
        // Request blocked.
        // Handle the exception (e.g. retry or fallback).
    }
}

lookProcessChain

Sentinel的核心功能是使用的是责任链模式实现,lookProcessChain(resourceWrapper)的主要作用就是用来构造责任链,源码如下:

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 根据资源的唯一标识来做本地缓存
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    if (chain == null) {
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // 限制资源资对应调用链的总数,一个资源对应一条调用链
                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;
}

进一步跟进方法会发现,责任链是由SlotChainBuilder#build()去构建的,默认实现类是DefaultSlotChainBuilder,源码如下:

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        // 找到ProcessorSlot所有的实现类,并排序
        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);
        for (ProcessorSlot slot : sortedSlotList) {
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }
            // 将功能槽放到责任链最后
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

老版本直接是硬编码方式:

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        chain.addLast(new NodeSelectorSlot());
        chain.addLast(new ClusterBuilderSlot());
        chain.addLast(new LogSlot());
        chain.addLast(new StatisticSlot());
        chain.addLast(new AuthoritySlot());
        chain.addLast(new SystemSlot());
        chain.addLast(new FlowSlot());
        chain.addLast(new DegradeSlot());

        return chain;
    }
}

以下内容来自文档

  • NodeSelectorSlot: 负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级;
  • ClusterBuilderSlot: 则用于存储资源的统计信息以及调用者信息,例如该资源的 RT, QPS, thread count 等等,这些信息将用作为多维度限流,降级的依据;
  • StatisticSlot: 则用于记录、统计不同纬度的 runtime 指标监控信息;
  • FlowSlot: 则用于根据预设的限流规则以及前面 slot 统计的状态,来进行流量控制;
  • AuthoritySlot: 则根据配置的黑白名单和调用来源信息,来做黑白名单控制;
  • DegradeSlot: 则通过统计信息以及预设的规则,来做熔断降级;
  • SystemSlot: 则通过系统的状态,例如 load1 等,来控制总的入口流量;

总结

  1. Sentinel 通过责任链模式,将各功能块隔离,即清晰划分出了各功能块的职责边界,也非常方便扩展。新增功能直接新增功能插槽就行了,不需要改以前代码。
  2. Sentinel 的本地缓存使用的是HashMap,通过加锁和写复制的思想来解决HashMap的线程安全性问题,在读远大于写的场景这种方式非常值得借鉴。

参考

https://github.com/alibaba/Sentinel/wiki/Sentinel-%E6%A0%B8%E5%BF%83%E7%B1%BB%E8%A7%A3%E6%9E%90

### 阿里巴巴 Sentinel 使用教程及相关文档 #### 什么是 SentinelSentinel 是阿里巴巴开源的一款面向分布式服务架构的流量控制、熔断降级组件,能够帮助开发者保障系统的稳定性。它提供了实时监控、限流、降级以及系统保护等功能,适用于微服务治理场景[^2]。 #### 主要特性 1. **流量控制**:支持基于 QPS 和并发线程数两种模式的限流策略。 2. **熔断降级**:当调用链路中的某个环节不可用或者响应时间过长时,会对整个请求进行快速失败处理。 3. **系统负载保护**:根据 CPU 负载或其他自定义指标对入口流量进行控制。 4. **实时监控与告警**:提供丰富的仪表盘展示当前系统的运行状态。 5. **扩展性强**:支持多种框架适配(如 Spring Cloud、Dubbo 等),并允许用户自定义规则和处理器逻辑[^3]。 #### 安装与配置指南 以下是使用 Alibaba Sentinel 的基本流程: ##### 1. 添加 Maven 依赖 在项目的 `pom.xml` 文件中添加以下依赖项: ```xml <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>最新版本号</version> </dependency> ``` ##### 2. 启动 Sentinel 控制台 下载官方提供的 Sentinel Dashboard 并启动服务端程序。可以通过访问 http://localhost:8080 进入管理界面[^1]。 ##### 3. 注解方式实现资源保护 下面是一个简单的例子,展示了如何利用 `@SentinelResource` 注解来定义受保护的业务方法,并指定异常处理函数: ```java import com.alibaba.csp.sentinel.annotation.SentinelResource; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") @SentinelResource(value = "hello", blockHandler = "handleBlock") public String hello() { return "Hello, world!"; } public String handleBlock(BlockException ex) { return "Blocked by Sentinel: " + ex.getClass().getSimpleName(); } } ``` 上述代码片段表明,在 `/hello` 接口上应用了名为 `"hello"` 的资源限制条件;如果发生阻塞事件,则触发 `handleBlock()` 方法返回定制化的错误消息[^1]。 ##### 4. 自定义规则设置 可以手动编写 Java 代码动态调整限流参数,如下所示: ```java import com.alibaba.csp.sentinel.Entry; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.slots.block.flow.FlowRule; import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; import java.util.ArrayList; import java.util.List; public class FlowControlExample { public static void main(String[] args) throws InterruptedException { initFlowRules(); while (true){ Entry entry = null; try{ entry = SphU.entry("custom_resource"); System.out.println("Passed..."); }catch(Exception e){ System.err.println(e.getMessage()); }finally{ if(entry !=null){ entry.exit(); } } Thread.sleep(10); } } private static void initFlowRules(){ List<FlowRule> rules=new ArrayList<>(); FlowRule rule=new FlowRule(); rule.setResource("custom_resource"); rule.setGrade(com.alibaba.csp.sentinel.slots.block.flow.RuleConstant.FLOW_GRADE_QPS); rule.setCount(2); // 设置每秒最多允许两个请求 rules.add(rule); FlowRuleManager.loadRules(rules); } } ``` 此示例演示了创建一个新的资源名称为 `"custom_resource"` 的限流规则,并将其阈值设定为每秒钟只接受两次请求[^4]。 #### 参考链接 更多详细资料可查阅官方文档页面:https://github.com/alibaba/Sentinel/wiki 相关问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值