Sentinel 学习

整理自:【尚硅谷】Sentinel视频教程丨Alibaba流量控制组件sentinel_哔哩哔哩_bilibili

分布式防控场景-流量控制

Sentinel的核心骨架是ProcessSlotChain,将不同的Slot按照顺序串在一起(责任链模式),从而实现各个功能(系统保护、流量控制、熔断降级)

NodeSelectorSlot:收集资源路径,以树状存储,用于根据调用路径来进行限流

ClusterBuilderSlot: 用于存储资源统计信息以及调用者信息,例如资源RT、QPS、Thread Count等,用于后续多维度限流、降级等

StatisticSlot:用于记录、统计不同维度的runtime指标监控信息

SystemSlot:对应系统规则

DegradeSlot:对应降级规则

AuthoritySlot:对应授权规则

FlowSlot:对应流控规则

ParamFlowSlot: 对应热点流控

1. SPI机制

SPI:服务处理接口[Service Provider Interface],原本是JDK里面的一个服务发现机制,一般都会将其进行扩展,如:Dubbo、Sentinel,因为原生的SPI机制有些缺陷「会加载配置文件里面所有的类」

Sentienl槽链中各Slot的顺序是固定好的,但是绝不是不能改变的。

Sentinel将ProcessSlot作为SPI接口进行扩展,使得slotchain有了扩展的能力,用户可以自定义Slot并编排顺序

2. 理解NodeSelectorSlot

会创建Invocation Tree,一个应用有一个Root节点

 

 另外:Context是调用链路上下文,用于数据在不同的Slot之间传递

从当前线程ThreadLocal获取 Context

即一个请求会占用一个线程,一个线程会绑定一个Context

3. Node之间的关系

Node:用于完成数据统计的接口

StatisticNode:统计节点,是Node的实现类,用于完成数据统计

EntranceNode:入口节点,一个Context会有一个入口节点,用于统计当前Context的总体流量数据

DefaultNode:默认节点,用于统计一个资源在当前Context中的总体流量数据

ClusterNode:集群节点,用于统计一个资源在所有Context中的总体流量数据

4. 创建资源对象

    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
        throws BlockException {

        // 从ThreadLocal里面获取Context
        // 即一个请求会占用一个线程,一个线程会绑定一个Context
        Context context = ContextUtil.getContext();

        // 若Context是NullContext类型,则表示当前系统中的Context数量超过了阈值
        // 即访问请求的数量已经超出了阈值,返回一个无需做规则检测的资源对象
        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);
        }

        // 若当前线程没有绑定Context,则创建一个Context并将其放到ThreadLocal里面
        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);
        }

        // 查找SlotChain
        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.
         */
        // 若没有找到chain,则意味着chain数量超过了阈值,直接返回一个无需做规则检测的资源对象
        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;
    }

5. Context

要确定一个Context需要两个属性:名称和来源

    protected static Context trueEnter(String name, String origin) {

        // 尝试着从ThreadLocal中获取Context
        Context context = contextHolder.get();

        // 若ThreadLocal中没有Context,则尝试着从缓存map中获取
        if (context == null) {
            // 缓存map的key为Context名称,value为EntranceNode
            Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;

            // 获取EntranceNode【当然:目的是为了构建Context】
            // 双重检测锁DCL,为了防止并发创建
            DefaultNode node = localCacheNameMap.get(name);
            if (node == null) {

                // 若缓存map的size 大于Context数量的最大阈值,则直接返回NULL_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 {

                                // 创建一个EntranceNode
                                node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                                // Add entrance node.
                                // 将新建的node添加到ROOT
                                Constants.ROOT.addChild(node);

                                // 将新建map写入到缓存map
                                // 为了防止"迭代稳定性问题" - iterator stable ,也叫写时复制(copy on write)
                                // 对于共享集合的写操作
                                // 如果不这样做的话,可能会引发读脏数据,这边没写完,那边已经走了
                                Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                                newMap.putAll(contextNameNodeMap);
                                newMap.put(name, node);
                                contextNameNodeMap = newMap;
                            }
                        }
                    } finally {
                        LOCK.unlock();
                    }
                }
            }

            // 根据EntranceNode和name构建Context
            context = new Context(node, name);

            // 初始化Context的来源
            context.setOrigin(origin);

            // 将Context设置到ThreadLocal中去
            contextHolder.set(context);
        }

        return context;
    }

6. lookProcessChain【这里得到的是一个链表】

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        // 从缓存map中获取当前资源的SlotChain
        // 缓存map的key是资源,value为其相关的SlotChain
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);

        // DCL 防止并发创建对象问题
        // 若缓存中没有相关的SlotChain,则创建一个并放入缓存
        if (chain == null) {
            synchronized (LOCK) {
                chain = chainMap.get(resourceWrapper);
                if (chain == null) {
                    // Entry size limit.
                    // 缓存map的size >= chain数量的最大阈值,则直接返回null。不在创建chain
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
                    }

                    // 创建新的chain
                    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;
    }

 

@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

@Spi(isSingleton = false, order = Constants.ORDER_CLUSTER_BUILDER_SLOT)
public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

从这里可以看出:order = Constants.ORDER_NODE_SELECTOR_SLOT 代表优先级,越小/优先级越高。且,如上这些都是系统已经定义好的,所以就会出现一个请求特定顺序经历Slot的顺序

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

        // 通过SPI进行构建Slot,这里会构建较多,构建List
        // 这里会加载所有的Slot
        List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
        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;
    }

那么如何理解addLast?

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }
// 这是一个单向链表,默认包含一个节点,且有两个指针first end同时指向了这个节点
public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

        @Override
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
            throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
        }

        @Override
        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }

    };
    AbstractLinkedProcessorSlot<?> end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }

    /**
     * Same as {@link #addLast(AbstractLinkedProcessorSlot)}.
     *
     * @param next processor to be added.
     */
    @Override
    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        addLast(next);
    }

    @Override
    public AbstractLinkedProcessorSlot<?> getNext() {
        return first.getNext();
    }

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
        throws Throwable {
        first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        first.exit(context, resourceWrapper, count, args);
    }

}

7. chain.entry 对资源操作

    public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
        throws Throwable {
        // 转向第一个节点,而非默认创建的
        first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
    }
void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
        throws Throwable {
        T t = (T)o;
        entry(context, resourceWrapper, t, count, prioritized, args);
    }

8. 来到NodeSelectorSlot【创建调用树】ROOT有,EntranceNode在Context里面,

所以这里核心是用来创建defaultNode的

public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        /*
         * It's interesting that we use context name rather resource name as the map key.
         *
         * Remember that same resource({@link ResourceWrapper#equals(Object)}) will share
         * the same {@link ProcessorSlotChain} globally, no matter in which context. So if
         * code goes into {@link #entry(Context, ResourceWrapper, DefaultNode, int, Object...)},
         * the resource name must be same but context name may not.
         *
         * If we use {@link com.alibaba.csp.sentinel.SphU#entry(String resource)} to
         * enter same resource in different context, using context name as map key can
         * distinguish the same resource. In this case, multiple {@link DefaultNode}s will be created
         * of the same resource name, for every distinct context (different context name) each.
         *
         * Consider another question. One resource may have multiple {@link DefaultNode},
         * so what is the fastest way to get total statistics of the same resource?
         * The answer is all {@link DefaultNode}s with same resource name share one
         * {@link ClusterNode}. See {@link ClusterBuilderSlot} for detail.
         */

        // 从缓存中获取DefaultNode
        DefaultNode node = map.get(context.getName());

        //DCL
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {

                    // 创建一个DefaultNode,并且放入缓存中
                    node = new DefaultNode(resourceWrapper, null);
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;
                    // Build invocation tree
                    //将新建Node添加到调用树中
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }

            }
        }

        context.setCurNode(node);
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

9. 来到ClusterBuilderSlot

构建ClusterBuilderSlot阶段

10. 来到StatisticSlot

11. 来到FlowSlot

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 检测并应用流控规则
        checkFlow(resourceWrapper, context, node, count, prioritized);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
    void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }
public abstract class AbstractRule implements Rule {

    /**
     * Resource name.
     */
    // 资源名称
    private String resource;

    /**
     * <p>
     * Application name that will be limited by origin.
     * The default limitApp is {@code default}, which means allowing all origin apps.
     * </p>
     * <p>
     * For authority rules, multiple origin name can be separated with comma (',').
     * </p>
     */
    // 请求来源
    private String limitApp;

 

public class FlowRule extends AbstractRule {

    public FlowRule() {
        super();
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    public FlowRule(String resourceName) {
        super();
        setResource(resourceName);
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    /**
     * The threshold type of flow control (0: thread count, 1: QPS).
     */
    // 阈值类型 0 线程数 1 QPS
    private int grade = RuleConstant.FLOW_GRADE_QPS;

    /**
     * Flow control threshold count.
     */
    private double count;

    /**
     * Flow control strategy based on invocation chain.
     *
     * {@link RuleConstant#STRATEGY_DIRECT} for direct flow control (by origin);
     * {@link RuleConstant#STRATEGY_RELATE} for relevant flow control (with relevant resource);
     * {@link RuleConstant#STRATEGY_CHAIN} for chain flow control (by entrance resource).
     */
    // 流控模式 直接、关联、链路
    private int strategy = RuleConstant.STRATEGY_DIRECT;

    /**
     * Reference resource in flow control with relevant resource or context.
     */
    // 关联流控模式下,关联的资源
    private String refResource;

    /**
     * Rate limiter control behavior.
     * 0. default(reject directly), 1. warm up, 2. rate limiter, 3. warm up + rate limiter
     */
    // 流控效果 0 快速失败 1 warm up(令牌桶算法) 2 rate limiter(漏斗算法) 3 1+2
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;

    // warm up 预热时长
    private int warmUpPeriodSec = 10;

    // 排队等待超时时长
    /**
     * Max queueing time in rate limiter behavior.
     */
    private int maxQueueingTimeMs = 500;

    // 是否是集群模式
    private boolean clusterMode;
    /**
     * Flow rule config for cluster mode.
     */
    private ClusterFlowConfig clusterConfig;

 12. ruleProvider.apply

    private final Function<String, Collection<FlowRule>> ruleProvider = new Function<String, Collection<FlowRule>>() {
        @Override
        public Collection<FlowRule> apply(String resource) {
            // Flow rule map should not be null.
            // 获取到所有资源的流控规则
            // map的key为资源名称,value为该资源上加载的所有流控规则
            Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();

            // 获取指定资源的所有流控规则
            return flowRules.get(resource);
        }
    };

13. canPassCheck

public boolean canPassCheck(/*@NonNull*/ FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                                    boolean prioritized) {
        // 从规则中获取限定的来源
        String limitApp = rule.getLimitApp();

        // 若限流的来源为null,则请求直接通过
        if (limitApp == null) {
            return true;
        }

        // 使用流控处理集群
        if (rule.isClusterMode()) {
            return passClusterCheck(rule, context, node, acquireCount, prioritized);
        }

        // 使用规则处理单机流控
        return passLocalCheck(rule, context, node, acquireCount, prioritized);
    }
private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                          boolean prioritized) {
        // 选择出合适的规则Node
        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
        // 若没有选择出node,说明没有规则,则直接返回true,表示检测通过
        if (selectedNode == null) {
            return true;
        }

        // 使用规则进行逐项检测
        return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
    }
public boolean canPass(Node node, int acquireCount, boolean prioritized) {

        // 获取当前时间窗中已经统计的数据
        int curCount = avgUsedTokens(node);

        // 若总数据量大于count,那么返回false,代表没有通过检测
        // 若小于等于阈值,则返回true,表示通过检测
        if (curCount + acquireCount > count) {
            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
                long currentTime;
                long waitInMs;
                currentTime = TimeUtil.currentTimeMillis();
                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    node.addOccupiedPass(acquireCount);
                    sleep(waitInMs);

                    // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }
    private int avgUsedTokens(Node node) {
        // 若没有选择出Node,则代表不需要统计数据,直接返回0
        if (node == null) {
            return DEFAULT_AVG_USED_TOKENS;
        }

        // 若阈值类型是线程数,则直接返回当前的线程数量
        // 若阈值类型为qps,则直接返回当前的qps
        return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
    }

14. DegradeSlot

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 触发熔断降级检测
        performChecking(context, resourceWrapper);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
// Sentinel1.8 中, 将三种熔断策略(慢调用、异常比、异常数) 封装为两种熔断器
public interface CircuitBreaker {

    /**
     * Get the associated circuit breaking rule.
     *
     * @return associated circuit breaking rule
     */
    // 获取降级规则
    DegradeRule getRule();

    /**
     * Acquires permission of an invocation only if it is available at the time of invoking.
     *
     * @param context context of current invocation
     * @return {@code true} if permission was acquired and {@code false} otherwise
     */
    // 判断请求是否可以通过
    // 返回true表示不用降级
    boolean tryPass(Context context);

    /**
     * Get current state of the circuit breaker.
     *
     * @return current state of the circuit breaker
     */
    // 熔断器状态
    State currentState();

    /**
     * <p>Record a completed request with the context and handle state transformation of the circuit breaker.</p>
     * <p>Called when a <strong>passed</strong> invocation finished.</p>
     *
     * @param context context of current invocation
     */
    // on开头的一般是回调 当请求通过并完成后会触发
    void onRequestComplete(Context context);

    /**
     * Circuit breaker state.
     */
    enum State {
        /**
         * In {@code OPEN} state, all requests will be rejected until the next recovery time point.
         */
        // 打开状态,会拒绝所有的请求
        OPEN,
        /**
         * In {@code HALF_OPEN} state, the circuit breaker will allow a "probe" invocation.
         * If the invocation is abnormal according to the strategy (e.g. it's slow), the circuit breaker
         * will re-transform to the {@code OPEN} state and wait for the next recovery time point;
         * otherwise the resource will be regarded as "recovered" and the circuit breaker
         * will cease cutting off requests and transform to {@code CLOSED} state.
         */
        // 过度状态
        HALF_OPEN,
        /**
         * In {@code CLOSED} state, all requests are permitted. When current metric value exceeds the threshold,
         * the circuit breaker will transform to {@code OPEN} state.
         */
        // 关闭状态,所有请求可以关闭
        CLOSED
    }
}

15. 时间窗限流算法

并不能保证任一时间段都是合理的,然后引入了滑动时间窗算法

 会浪费统计资源,影响效率,因为有大量重复统计数据

 

 

### 关于 Sentinel学习资料 #### 一、基础概念与功能概述 Sentinel 是阿里巴巴开源的一款用于流量防护和服务治理的工具,主要提供限流、熔断降级等功能。其核心机制通过 `CommonFilter` 过滤器实现业务逻辑拦截和处理[^1]。 #### 二、配置与集成方式 在实际项目中,可以通过 Maven 引入依赖来完成 Sentinel 对 Nacos 的监听支持。例如,在 `order-service` 中可以加入如下依赖: ```xml <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> ``` 上述代码片段展示了如何将 Sentinel 数据源绑定到 Nacos 上,从而动态调整规则配置[^2]。 #### 三、与其他框架比较 为了更好地理解 Sentinel 的特性,可将其与 Hystrix 做对比分析。以下是两者的主要区别: | 特性 | **Sentinel** | **Hystrix** | |-----------------|----------------------------------|------------------------------| | 隔离策略 | 支持信号量隔离 | 主要采用线程池隔离 | | 动态规则更新 | 提供多种数据源适配 | 默认不支持 | 此表清晰地反映了两者的差异所在[^3]。 #### 四、常见错误及其解决方法 当遇到诸如 “Blocked by Sentinel (flow limiting)” 类型的错误提示时,通常是因为触发了预设的限流条件所致。此时可以在 Sentinel 控制台中的簇点链路模块下找到对应资源并设置合理的阈值参数,比如降低异常比例或者延长观察窗口的时间长度等措施加以优化[^4]。 #### 五、推荐参考资料列表 对于希望深入研究该主题的学习者而言,可以从以下几个方面入手获取更多信息: 1. 官方文档始终是最权威的第一手材料; 2. 社区分享的技术博客往往包含大量实战经验总结; 3. GitHub 上的相关仓库也可能藏有不少实用脚本样例可供参考。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值