微服务架构 | 5.4 Sentinel 流控、统计和熔断的源码分析(经典)

目录


前言

参考资料
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服务原理与实战》
《B站 尚硅谷 SpringCloud 框架开发教程 周阳》
《Sentinel GitHub 官网》
《Sentinel 官网》

调用链路是 Sentinel 的工作主流程,由各个 Slot 槽组成,将不同的 Slot 槽按照顺序串在一起,从而将不同的功能(限流、降级、系统保护)组合在一起;

本篇《2. 获取 ProcessorSlot 链》将从源码级讲解如何获取调用链路,接着会以遍历链表的方式处理每一个 Slot 槽,其中就有:FlowSlot、StatisticSlot、DegradeSlot 等。分别对应本篇《3. 流控槽实施流控逻辑》、《4. 统计槽实施指标数据统计》和《5. 熔断槽实施服务熔断》;


1. Sentinel 的自动装配

1.2 依赖引入

  • 我们引入 Sentinel 的 starter 依赖文件,不需要太多额外操作,即可使用 Sentinel 默认自带的限流功能,原因是这些配置和功能都给我们自动装配了;
  • 在 Spring-Cloud-Alibaba-Sentinel 包下的 META-INF/spring.factories 文件里定义了会自动装配哪些类;

Sentinel 的自动装配

  • SentinelWebAutoConfiguration:对 Web Servlet 环境的支持;
  • SentinelWebFluxAutoConfiguration:对 Spring WebFlux 的支持;
  • SentinelEndpointAutoConfiguration:暴露 Endpoint 信息;
  • SentinelFeignAutoConfiguration:用于适应 Feign 组件;
  • SentinelAutoConfiguration:支持对 RestTemplate 的服务调用使用 Sentinel 进行保护;

1.3 SentinelWebAutoConfiguration 配置类

  • 在 SentinelWebAutoConfiguration 配置类中自动装配了一个 FilterRegistrationBean,其主要作用是注册一个 CommonFilter,并且默认情况下通过 /* 规则拦截所有的请求;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Configuration</span>
<span style="color:#2b91af">@EnableConfigurationProperties(SentinelProperties.class)</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">SentinelWebAutoConfiguration</span> {
    
    <span style="color:#008000">//省略其他代码</span>
    
	<span style="color:#2b91af">@Bean</span>
	<span style="color:#2b91af">@ConditionalOnProperty(name = "spring.cloud.sentinel.filter.enabled", matchIfMissing = true)</span>
	<span style="color:#0000ff">public</span> FilterRegistrationBean <span style="color:#a31515">sentinelFilter</span>() {
		FilterRegistrationBean<Filter> registration = <span style="color:#0000ff">new</span> <span style="color:#a31515">FilterRegistrationBean</span><>();

		SentinelProperties.<span style="color:#a31515">Filter</span> <span style="color:#008000">filterConfig</span> <span style="color:#ab5656">=</span> properties.getFilter();

		<span style="color:#0000ff">if</span> (filterConfig.getUrlPatterns() == <span style="color:#a31515">null</span> || filterConfig.getUrlPatterns().isEmpty()) {
			List<String> defaultPatterns = <span style="color:#0000ff">new</span> <span style="color:#a31515">ArrayList</span><>();
			<span style="color:#008000">//默认情况下通过 /* 规则拦截所有的请求</span>
			defaultPatterns.add(<span style="color:#a31515">"/*"</span>);
			filterConfig.setUrlPatterns(defaultPatterns);
		}

		registration.addUrlPatterns(filterConfig.getUrlPatterns().toArray(<span style="color:#0000ff">new</span> <span style="color:#a31515">String</span>[<span style="color:#880000">0</span>]));
		<span style="color:#008000">//【点进去】注册 CommonFilter</span>
		<span style="color:#a31515">Filter</span> <span style="color:#008000">filter</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">CommonFilter</span>();
		registration.setFilter(filter);
		registration.setOrder(filterConfig.getOrder());
		registration.addInitParameter(<span style="color:#a31515">"HTTP_METHOD_SPECIFY"</span>, String.valueOf(properties.getHttpMethodSpecify()));
		log.info(<span style="color:#a31515">"[Sentinel Starter] register Sentinel CommonFilter with urlPatterns: {}."</span>, filterConfig.getUrlPatterns());
		<span style="color:#0000ff">return</span> registration;
	}
}
</code></span></span>

1.4 CommonFilter 过滤器

  • CommonFilter 过滤器的作用与源码如下:
    • 从请求中获取目标 URL;
    • 获取 Urlcleaner;
    • 对当前 URL 添加限流埋点;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">CommonFilter</span> <span style="color:#0000ff">implements</span> <span style="color:#a31515">Filter</span> {
    
    <span style="color:#008000">//省略部分代码</span>

    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">doFilter</span>(ServletRequest request, ServletResponse response, FilterChain chain) <span style="color:#0000ff">throws</span> IOException, ServletException {
        <span style="color:#a31515">HttpServletRequest</span> <span style="color:#008000">sRequest</span> <span style="color:#ab5656">=</span> (HttpServletRequest)request;
        <span style="color:#a31515">Entry</span> <span style="color:#008000">urlEntry</span> <span style="color:#ab5656">=</span> <span style="color:#a31515">null</span>;
        <span style="color:#0000ff">try</span> {
            <span style="color:#008000">//解析请求 URL</span>
            <span style="color:#a31515">String</span> <span style="color:#008000">target</span> <span style="color:#ab5656">=</span> FilterUtil.filterTarget(sRequest);
            <span style="color:#008000">//URL 清洗</span>
            <span style="color:#a31515">UrlCleaner</span> <span style="color:#008000">urlCleaner</span> <span style="color:#ab5656">=</span> WebCallbackManager.getUrlCleaner();
            <span style="color:#0000ff">if</span> (urlCleaner != <span style="color:#a31515">null</span>) {
                <span style="color:#008000">//如果存在,则说明配置过 URL 清洗策略,替换配置的 targer</span>
                target = urlCleaner.clean(target);
            }
            <span style="color:#0000ff">if</span> (!StringUtil.isEmpty(target)) {
                <span style="color:#a31515">String</span> <span style="color:#008000">origin</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">this</span>.parseOrigin(sRequest);
                ContextUtil.enter(<span style="color:#a31515">"sentinel_web_servlet_context"</span>, origin);
                <span style="color:#0000ff">if</span> (<span style="color:#0000ff">this</span>.httpMethodSpecify) {
                    <span style="color:#a31515">String</span> <span style="color:#008000">pathWithHttpMethod</span> <span style="color:#ab5656">=</span> sRequest.getMethod().toUpperCase() + <span style="color:#a31515">":"</span> + target;
                    <span style="color:#008000">//使用 SphU.entry() 方法对 URL 添加限流埋点</span>
                    urlEntry = SphU.entry(pathWithHttpMethod, <span style="color:#880000">1</span>, EntryType.IN);
                } <span style="color:#0000ff">else</span> {
                    urlEntry = SphU.entry(target, <span style="color:#880000">1</span>, EntryType.IN);
                }
            }
            <span style="color:#008000">//执行过滤</span>
            chain.doFilter(request, response);
        } <span style="color:#0000ff">catch</span> (BlockException var14) {
            <span style="color:#a31515">HttpServletResponse</span> <span style="color:#008000">sResponse</span> <span style="color:#ab5656">=</span> (HttpServletResponse)response;
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, var14);
        } <span style="color:#0000ff">catch</span> (ServletException | RuntimeException | IOException var15) {
            Tracer.traceEntry(var15, urlEntry);
            <span style="color:#0000ff">throw</span> var15;
        } <span style="color:#0000ff">finally</span> {
            <span style="color:#0000ff">if</span> (urlEntry != <span style="color:#a31515">null</span>) {
                urlEntry.exit();
            }
            ContextUtil.exit();
        }
    }
}
</code></span></span>

1.5 小结

  • 对于 Web Servlet 环境,只是通过 Filter 的方式将所有请求自动设置为 Sentinel 的资源,从而达到限流的目的;

2. 获取 ProcessorSlot 链

  • Sentinel 的工作原理主要依靠 ProcessorSlot 链,遍历链中的每一个 Slot 槽,执行相应逻辑;

2.1 Sentinel 源码包结构

  • 在 DeBug 之前,我们需要对 Sentinel 的源码包结构做个分析,以找到方法的入口;
模块名说明
sentinel-adapter负责针对主流开源框架进行限流适配,如:Dubbo、gRPC、Zuul 等;
sentinel-coreSentinel 核心库,提供限流、熔断等实现;
sentinel-dashboard控制台模块,提供可视化监控和管理;
sentinel-demo官方案例;
sentinel-extension实现不同组件的数据源扩展,如:Nacos、ZooKeeper、Apollo 等;
sentinel-transport通信协议处理模块;
  • Slot 槽是 Sentinel 的核心,因此方法的入口在 sentinel-core 核心库,里面有好多个 SphU.entry() 方法,我们给方法打上断点,DeBug 进入,然后登录 Sentinel 控制台;

首次DeBug 进入 SphU.entry() 方法

2.2 获取 ProcessorSlot 链与操作 Slot 槽的入口 CtSph.entryWithPriority()

  • 一直进入最终方法的实现在 CtSph.entryWithPriority() 方法里,其主要逻辑与源码如下:
    • 校验全局上下文 context;
    • 构造 ProcessorSlot 链;
    • 遍历 ProcessorSlot 链操作 Slot 槽(遍历链表);
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">private</span> Entry <span style="color:#a31515">entryWithPriority</span>(ResourceWrapper resourceWrapper, <span style="color:#a31515">int</span> count, <span style="color:#a31515">boolean</span> prioritized, Object... args) <span style="color:#0000ff">throws</span> BlockException {
    <span style="color:#a31515">Context</span> <span style="color:#008000">context</span> <span style="color:#ab5656">=</span> ContextUtil.getContext();
    <span style="color:#0000ff">if</span> (context <span style="color:#0000ff">instanceof</span> NullContext) {
        <span style="color:#008000">//上下文量已经超过阈值 -> 只初始化条目,不进行规则检查</span>
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">CtEntry</span>(resourceWrapper, <span style="color:#a31515">null</span>, context);
    }

    <span style="color:#0000ff">if</span> (context == <span style="color:#a31515">null</span>) {
        <span style="color:#008000">//没有指定上下文 -> 使用默认上下文 context</span>
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }
     
     <span style="color:#0000ff">if</span> (!Constants.ON) {
        <span style="color:#008000">//全局开关关闭 -> 没有规则检查</span>
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">CtEntry</span>(resourceWrapper, <span style="color:#a31515">null</span>, context);
    }
    <span style="color:#008000">//【断点步入 2.2.1】通过 lookProcessChain 方法获取 ProcessorSlot 链</span>
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);


    <span style="color:#0000ff">if</span> (chain == <span style="color:#a31515">null</span>) {
        <span style="color:#008000">//表示资源量超过 Constants.MAX_SLOT_CHAIN_SIZE 常量 -> 不会进行规则检查</span>
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">CtEntry</span>(resourceWrapper, <span style="color:#a31515">null</span>, context);
    }

    <span style="color:#a31515">Entry</span> <span style="color:#008000">e</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">CtEntry</span>(resourceWrapper, chain, context);
    <span style="color:#0000ff">try</span> {
        <span style="color:#008000">//【断点步入 3./4./5.】执行 ProcessorSlot 对 ProcessorSlot 链中的 Slot 槽遍历操作(遍历链表的方式)</span>
        chain.entry(context, resourceWrapper, <span style="color:#a31515">null</span>, count, prioritized, args);
    } <span style="color:#0000ff">catch</span> (BlockException e1) {
        e.exit(count, args);
        <span style="color:#0000ff">throw</span> e1;
    } <span style="color:#0000ff">catch</span> (Throwable e1) {
        <span style="color:#008000">//这种情况不应该发生,除非 Sentinel 内部存在错误</span>
        RecordLog.info(<span style="color:#a31515">"Sentinel unexpected exception"</span>, e1);
    }
    <span style="color:#0000ff">return</span> e;
}
</code></span></span>

2.2.1 构造 ProcessorSlot 链 CtSph.lookProcessChain()

  • 进入 CtSph.lookProcessChain() 方法;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java">ProcessorSlot<Object> <span style="color:#a31515">lookProcessChain</span>(ResourceWrapper resourceWrapper) {
    <span style="color:#008000">//从缓存中获取 slot 调用链</span>
    <span style="color:#a31515">ProcessorSlotChain</span> <span style="color:#008000">chain</span> <span style="color:#ab5656">=</span> chainMap.get(resourceWrapper);
    <span style="color:#0000ff">if</span> (chain == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">synchronized</span> (LOCK) {
            chain = chainMap.get(resourceWrapper);
            <span style="color:#0000ff">if</span> (chain == <span style="color:#a31515">null</span>) {
                <span style="color:#008000">// Entry size limit.</span>
                <span style="color:#0000ff">if</span> (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    <span style="color:#0000ff">return</span> <span style="color:#a31515">null</span>;
                }
                <span style="color:#008000">//【断点步入】构造 Slot 链(责任链模式)</span>
                chain = SlotChainProvider.newSlotChain();
                Map<ResourceWrapper, ProcessorSlotChain> newMap = <span style="color:#0000ff">new</span> <span style="color:#a31515">HashMap</span><ResourceWrapper, ProcessorSlotChain>(
                    chainMap.size() + <span style="color:#880000">1</span>);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    <span style="color:#0000ff">return</span> chain;
}
</code></span></span>
  • 最终调用 DefaultSlotChainBuilder.build() 方法构造 DefaultProcessorSlotChain;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> ProcessorSlotChain <span style="color:#a31515">build</span>() {
    <span style="color:#a31515">ProcessorSlotChain</span> <span style="color:#008000">chain</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">DefaultProcessorSlotChain</span>();
    List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
    <span style="color:#0000ff">for</span> (ProcessorSlot slot : sortedSlotList) {
        <span style="color:#0000ff">if</span> (!(slot <span style="color:#0000ff">instanceof</span> AbstractLinkedProcessorSlot)) {
            RecordLog.warn(<span style="color:#a31515">"The ProcessorSlot("</span> + slot.getClass().getCanonicalName() + <span style="color:#a31515">") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain"</span>);
            <span style="color:#0000ff">continue</span>;
        }
        chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
    }
    <span style="color:#0000ff">return</span> chain;
}
</code></span></span>
  • 可以看到最后 ProcessorSlotChain 链中有 10 个 Slot 插槽:
  • 在本篇笔记中我们关注 3 个槽:
    • FlowSlot:进行流控规则校验,对应本篇《3. 流控槽实施流控逻辑》;
    • StatisticSlot:实现指标数据的统计,对应本篇《4. 统计槽实施指标数据统计》;
    • DegradeSlot:服务熔断,对应本篇《5. 熔断槽实施服务熔断》

ProcessorSlotChain 链中有 10 个 Slot 插槽

2.2.2 操作 Slot 槽的入口

  • 操作 Slot 槽的入口方法是:ProcessorSlot.entry()
  • 接着会以遍历链表的方式操作每个 Slot 槽,其中就有:FlowSlot、StatisticSlot、DegradeSlot 等。分别对应下面的《3. 流控槽实施流控逻辑》、《4. 统计槽实施指标数据统计》和《5. 熔断槽实施服务熔断》;

3. 流控槽实施流控逻辑 FlowSlot.entry()

  • 进入 ProcessorSlot.entry() 方法,它会遍历每个 Slot 插槽,并对其进行操作,其中会经过 FlowSlot.entry() 方法(需要提前给该方法打上断点),方法的逻辑跟源码如下:
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">entry</span>(Context context, ResourceWrapper resourceWrapper, DefaultNode node, <span style="color:#a31515">int</span> count, <span style="color:#a31515">boolean</span> prioritized, Object... args) <span style="color:#0000ff">throws</span> Throwable {
    <span style="color:#008000">//【断点步入】检查流量规则</span>
    checkFlow(resourceWrapper, context, node, count, prioritized);
    <span style="color:#008000">//调用下一个 Slot</span>
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
</code></span></span>
  • 进入 FlowSlot.checkFlow() 方法,最终调用 FlowRuleChecker.checkFlow() 方法,方法的逻辑和源码如下:
    • 遍历所有流控规则 FlowRule;
    • 针对每个规则调用 canPassCheck 进行校验;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">checkFlow</span>(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                      Context context, DefaultNode node, <span style="color:#a31515">int</span> count, <span style="color:#a31515">boolean</span> prioritized) <span style="color:#0000ff">throws</span> BlockException {
    <span style="color:#0000ff">if</span> (ruleProvider == <span style="color:#a31515">null</span> || resource == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#008000">//【断点步入 3.1】获取流控规则</span>
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());
    <span style="color:#0000ff">if</span> (rules != <span style="color:#a31515">null</span>) {
        <span style="color:#008000">//遍历所有流控规则 FlowRule</span>
        <span style="color:#0000ff">for</span> (FlowRule rule : rules) {
            <span style="color:#008000">//【点进去 3.2】校验每条规则</span>
            <span style="color:#0000ff">if</span> (!canPassCheck(rule, context, node, count, prioritized)) {
                <span style="color:#0000ff">throw</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">FlowException</span>(rule.getLimitApp(), rule);
            }
        }
    }
}
</code></span></span>

3.1 获取流控规则 FlowSlot.ruleProvider.apply()

  • 进入 FlowSlot.ruleProvider.apply() 方法,获取到 Sentinel 控制台上的流控规则;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">private</span> <span style="color:#0000ff">final</span> Function<String, Collection<FlowRule>> ruleProvider = <span style="color:#0000ff">new</span> <span style="color:#a31515">Function</span><String, Collection<FlowRule>>() {
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> Collection<FlowRule> <span style="color:#a31515">apply</span>(String resource) {
        <span style="color:#008000">// Flow rule map should not be null.</span>
        Map<String, List<FlowRule>> flowRules = FlowRuleManager.getFlowRuleMap();
        <span style="color:#0000ff">return</span> flowRules.get(resource);
    }
};
</code></span></span>

3.2 校验每条规则 FlowRuleChecker.canPassCheck()

  • 进入 FlowRuleChecker.canPassCheck() 方法,分集群和单机模式校验每条规则;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#a31515">boolean</span> <span style="color:#a31515">canPassCheck</span>(<span style="color:#008000">/*@NonNull*/</span> FlowRule rule, Context context, DefaultNode node, <span style="color:#a31515">int</span> acquireCount, <span style="color:#a31515">boolean</span> prioritized) {
    <span style="color:#a31515">String</span> <span style="color:#008000">limitApp</span> <span style="color:#ab5656">=</span> rule.getLimitApp();
    <span style="color:#0000ff">if</span> (limitApp == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">return</span> <span style="color:#a31515">true</span>;
    }
    <span style="color:#008000">//集群模式</span>
    <span style="color:#0000ff">if</span> (rule.isClusterMode()) {
        <span style="color:#0000ff">return</span> passClusterCheck(rule, context, node, acquireCount, prioritized);
    }
    <span style="color:#008000">//【点进去】单机模式</span>
    <span style="color:#0000ff">return</span> passLocalCheck(rule, context, node, acquireCount, prioritized);
}
</code></span></span>
  • 由于我们是单机模式,进入 FlowRuleChecker.passLocalCheck() 方法,其主要逻辑和源码如下:
    • 根据来源和策略获取 Node,从而拿到统计的 runtime 信息;
    • 使用流量控制器检查是否让流量通过;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">private</span> <span style="color:#0000ff">static</span> <span style="color:#a31515">boolean</span> <span style="color:#a31515">passLocalCheck</span>(FlowRule rule, Context context, DefaultNode node, <span style="color:#a31515">int</span> acquireCount, <span style="color:#a31515">boolean</span> prioritized) {
    <span style="color:#008000">//【点进去 3.2.1】获取 Node</span>
    <span style="color:#a31515">Node</span> <span style="color:#008000">selectedNode</span> <span style="color:#ab5656">=</span> selectNodeByRequesterAndStrategy(rule, context, node);
    <span style="color:#0000ff">if</span> (selectedNode == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">return</span> <span style="color:#a31515">true</span>;
    }
    <span style="color:#008000">//【点进去 3.2.2】获取流控的处理策略</span>
    <span style="color:#0000ff">return</span> rule.getRater().canPass(selectedNode, acquireCount, prioritized);
}
</code></span></span>

3.2.1 获取 Node FlowRuleChecker.selectNodeByRequesterAndStrategy()

  • 进入 FlowRuleChecker.selectNodeByRequesterAndStrategy() 方法,其根据 FlowRule 中配置的 Strategy 和 limitApp 属性,返回不同处理策略的 Node;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">static</span> Node <span style="color:#a31515">selectNodeByRequesterAndStrategy</span>(<span style="color:#008000">/*@NonNull*/</span> FlowRule rule, Context context, DefaultNode node) {
    <span style="color:#008000">//limitApp 不能为空</span>
    <span style="color:#a31515">String</span> <span style="color:#008000">limitApp</span> <span style="color:#ab5656">=</span> rule.getLimitApp();
    <span style="color:#a31515">int</span> <span style="color:#008000">strategy</span> <span style="color:#ab5656">=</span> rule.getStrategy();
    <span style="color:#a31515">String</span> <span style="color:#008000">origin</span> <span style="color:#ab5656">=</span> context.getOrigin();
    
    <span style="color:#008000">//场景1:限流规则设置了具体应用,如果当前流量就是通过该应用的,则命中场景1</span>
    <span style="color:#0000ff">if</span> (limitApp.equals(origin) && filterOrigin(origin)) {
        <span style="color:#0000ff">if</span> (strategy == RuleConstant.STRATEGY_DIRECT) {
            <span style="color:#008000">// Matches limit origin, return origin statistic node.</span>
            <span style="color:#0000ff">return</span> context.getOriginNode();
        }
        <span style="color:#0000ff">return</span> selectReferenceNode(rule, context, node);
    } <span style="color:#0000ff">else</span> <span style="color:#0000ff">if</span> (RuleConstant.LIMIT_APP_DEFAULT.equals(limitApp)) {
    <span style="color:#008000">//场景2:限流规则未指定任何具体应,默认为default,则当前流量直接命中场景2</span>
        <span style="color:#0000ff">if</span> (strategy == RuleConstant.STRATEGY_DIRECT) {
            <span style="color:#008000">// Return the cluster node.</span>
            <span style="color:#0000ff">return</span> node.getClusterNode();
        }

        <span style="color:#0000ff">return</span> selectReferenceNode(rule, context, node);
    } <span style="color:#0000ff">else</span> <span style="color:#0000ff">if</span> (RuleConstant.LIMIT_APP_OTHER.equals(limitApp) && FlowRuleManager.isOtherOrigin(origin, rule.getResource())) {
    <span style="color:#008000">//场景3:限流规则设置的是other,当前流量未命中前两种场景</span>
        <span style="color:#0000ff">if</span> (strategy == RuleConstant.STRATEGY_DIRECT) {
            <span style="color:#0000ff">return</span> context.getOriginNode();
        }
        <span style="color:#0000ff">return</span> selectReferenceNode(rule, context, node);
    }
    <span style="color:#0000ff">return</span> <span style="color:#a31515">null</span>;
}
</code></span></span>
  • 假设我们对接口 UserService 配置限流 1000 QPS,这 3 种场景分别如下:
    • 场景 1:目的是优先保障重要来源的流量。我们需要区分调用来源,将限流规则细化。对A应用配置500QPS,对B应用配置200QPS,此时会产生两条规则:A应用请求的流量限制在500,B应用请求的流量限制在200;
    • 场景 2:没有特别重要来源的流量。我们不想区分调用来源,所有入口调用 UserService 共享一个规则,所有 client 加起来总流量只能通过 1000 QPS;
    • 场景 3:配合第1种场景使用,在长尾应用多的情况下不想对每个应用进行设置,没有具体设置的应用都将命中;

3.2.2 获取流控的处理策略 `FlowRule.getRater().canPass()

  • 进入 FlowRule.getRater().canPass() 方法,首先通过 FlowRule.getRater() 获得流控行为 TrafficShapingController,这是一个接口,有四种实现类,如下图所示:

TrafficShapingController 的四种实现类

  • 有以下四种处理策略:
    • DefaultController:直接拒绝;
    • RateLimiterController:匀速排队;
    • WarmUpController:冷启动(预热);
    • WarmUpRateLimiterController:匀速+冷启动。
  • 最终调用 TrafficShapingController.canPass() 方法,执行流控行为;

4. 统计槽实施指标数据统计 StatisticSlot.entry()

  • 限流的核心是限流算法的实现,Sentinel 默认采用滑动窗口算法来实现限流,具体的指标数据统计由 StatisticSlot 实现;
  • 我们给 StatisticSlot.entry() 方法里的语句打上断点,运行到光标处;
  • StatisticSlot.entry() 方法的核心是使用 Node 统计“增加线程数”和“请求通过数”;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">entry</span>(Context context, ResourceWrapper resourceWrapper, DefaultNode node, <span style="color:#a31515">int</span> count, <span style="color:#a31515">boolean</span> prioritized, Object... args) <span style="color:#0000ff">throws</span> Throwable {
    <span style="color:#0000ff">try</span> {
        <span style="color:#008000">//先执行后续 Slot 检查,再统计数据(即先调用后续所有 Slot)</span>
        fireEntry(context, resourceWrapper, node, count, prioritized, args);

        <span style="color:#008000">//【断点步入】使用 Node 统计“增加线程数”和“请求通过数”</span>
        node.increaseThreadNum();
        node.addPassRequest(count);

        <span style="color:#008000">//如果存在来源节点,则对来源节点增加线程数和请求通过数</span>
        <span style="color:#0000ff">if</span> (context.getCurEntry().getOriginNode() != <span style="color:#a31515">null</span>) {
            context.getCurEntry().getOriginNode().increaseThreadNum();
            context.getCurEntry().getOriginNode().addPassRequest(count);
        }
        
        <span style="color:#008000">//如果是入口流量,则对全局节点增加线程数和请求通过数</span>
        <span style="color:#0000ff">if</span> (resourceWrapper.getEntryType() == EntryType.IN) {
            Constants.ENTRY_NODE.increaseThreadNum();
            Constants.ENTRY_NODE.addPassRequest(count);
        }

        <span style="color:#008000">//执行事件通知和回调函数</span>
        <span style="color:#0000ff">for</span> (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
            handler.onPass(context, resourceWrapper, node, count, args);
        }
    <span style="color:#008000">//处理优先级等待异常    </span>
    } <span style="color:#0000ff">catch</span> (PriorityWaitException ex) {
        node.increaseThreadNum();
        <span style="color:#008000">//如果有来源节点,则对来源节点增加线程数</span>
        <span style="color:#0000ff">if</span> (context.getCurEntry().getOriginNode() != <span style="color:#a31515">null</span>) {
            context.getCurEntry().getOriginNode().increaseThreadNum();
        }

        <span style="color:#008000">//如果是入口流量,对全局节点增加线程数</span>
        <span style="color:#0000ff">if</span> (resourceWrapper.getEntryType() == EntryType.IN) {
            Constants.ENTRY_NODE.increaseThreadNum();
        }
        <span style="color:#008000">//执行事件通知和回调函数</span>
        <span style="color:#0000ff">for</span> (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
            handler.onPass(context, resourceWrapper, node, count, args);
        }
    <span style="color:#008000">//处理限流、熔断等异常    </span>
    } <span style="color:#0000ff">catch</span> (BlockException e) {
        
        <span style="color:#008000">//省略</span>
        
        <span style="color:#0000ff">throw</span> e;
    <span style="color:#008000">//处理业务异常    </span>
    } <span style="color:#0000ff">catch</span> (Throwable e) {
        context.getCurEntry().setError(e);
        <span style="color:#0000ff">throw</span> e;
    }
}
</code></span></span>

4.1 统计“增加线程数”和“请求通过数”

  • 这两个方法都是调用同一个类的,笔者以第一个为例,进入 DefaultNode.increaseThreadNum() 方法,最终调用的是 StatisticNode.increaseThreadNum(),而统计也是依靠 StatisticNode 维护的,这里放上 StatisticNode 的统计核心与源码:
    • StatisticNode 持有两个计数器 Metric 对象,统计行为是通过 Metric 完成的;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">StatisticNode</span> <span style="color:#0000ff">implements</span> <span style="color:#a31515">Node</span> {

    <span style="color:#008000">//省略其他代码</span>

    <span style="color:#008000">//【断点步入】最近 1s 滑动窗口计数器(默认 1s)</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">transient</span> <span style="color:#0000ff">volatile</span> <span style="color:#a31515">Metric</span> <span style="color:#008000">rollingCounterInSecond</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">ArrayMetric</span>(SampleCountProperty.SAMPLE_COUNT,
        IntervalProperty.INTERVAL);

    <span style="color:#008000">//最近 1min 滑动窗口计数器(默认 1min)</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">transient</span> <span style="color:#a31515">Metric</span> <span style="color:#008000">rollingCounterInMinute</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">ArrayMetric</span>(<span style="color:#880000">60</span>, <span style="color:#880000">60</span> * <span style="color:#880000">1000</span>, <span style="color:#a31515">false</span>);
    
    <span style="color:#008000">//增加 “请求通过数” </span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addPassRequest</span>(<span style="color:#a31515">int</span> count) {
        rollingCounterInSecond.addPass(count);
        rollingCounterInMinute.addPass(count);
    }
    <span style="color:#008000">//增加 RT 和成功数</span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addRtAndSuccess</span>(<span style="color:#a31515">long</span> rt, <span style="color:#a31515">int</span> successCount) {
        rollingCounterInSecond.addSuccess(successCount);
        rollingCounterInSecond.addRT(rt);
        rollingCounterInMinute.addSuccess(successCount);
        rollingCounterInMinute.addRT(rt);
    }

    <span style="color:#008000">//增加“线程数”</span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">increaseThreadNum</span>() {
        curThreadNum.increment();
    }
}
</code></span></span>
  • 这里还有减少请求通过数(线程数)、统计最大值等方法,由于篇幅有限,这里不放出,感兴趣的读者可以自己 DeBug 进入看看;

4.2 数据统计的数据结构

4.2.1 ArrayMetric 指标数组

  • ArrayMetric 的构造方法需要先给方法打上断点,重新 DeBug,在初始化时注入构造;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">ArrayMetric</span> <span style="color:#0000ff">implements</span> <span style="color:#a31515">Metric</span> {
    
    <span style="color:#008000">//省略其他代码</span>

    <span style="color:#008000">//【点进去 4.2.2】数据存储</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">final</span> LeapArray<MetricBucket> data;
    
    <span style="color:#008000">//最近 1s 滑动计数器用的是 OccupiableBucketLeapArray</span>
    <span style="color:#0000ff">public</span> <span style="color:#a31515">ArrayMetric</span>(<span style="color:#a31515">int</span> sampleCount, <span style="color:#a31515">int</span> intervalInMs) {
        <span style="color:#0000ff">this</span>.data = <span style="color:#0000ff">new</span> <span style="color:#a31515">OccupiableBucketLeapArray</span>(sampleCount, intervalInMs);
    }
    
    <span style="color:#008000">//最近 1min 滑动计数器用的是 BucketLeapArray</span>
    <span style="color:#0000ff">public</span> <span style="color:#a31515">ArrayMetric</span>(<span style="color:#a31515">int</span> sampleCount, <span style="color:#a31515">int</span> intervalInMs, <span style="color:#a31515">boolean</span> enableOccupy) {
        <span style="color:#0000ff">if</span> (enableOccupy) {
            <span style="color:#0000ff">this</span>.data = <span style="color:#0000ff">new</span> <span style="color:#a31515">OccupiableBucketLeapArray</span>(sampleCount, intervalInMs);
        } <span style="color:#0000ff">else</span> {
            <span style="color:#0000ff">this</span>.data = <span style="color:#0000ff">new</span> <span style="color:#a31515">BucketLeapArray</span>(sampleCount, intervalInMs);
        }
    }

    <span style="color:#008000">//增加成功数</span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addSuccess</span>(<span style="color:#a31515">int</span> count) {
        WindowWrap<MetricBucket> wrap = data.currentWindow();
        wrap.value().addSuccess(count);
    }

    <span style="color:#008000">//增加通过数</span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addPass</span>(<span style="color:#a31515">int</span> count) {
        WindowWrap<MetricBucket> wrap = data.currentWindow();
        wrap.value().addPass(count);
    }

    <span style="color:#008000">//增加 RT</span>
    <span style="color:#2b91af">@Override</span>
    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addRT</span>(<span style="color:#a31515">long</span> rt) {
        WindowWrap<MetricBucket> wrap = data.currentWindow();
        wrap.value().addRT(rt);
    }
}
</code></span></span>

4.2.2 LeapArray 环形数组

  • LeapArray 是处理数据的核心数据结构,采用滑动窗口算法;
  • ArrayMetric 中持有 LeapArray 对象,所有方法都是对 LeapArray 进行操作;
  • LeapArray 是环形的数据结构,为了节约内存,它存储固定个数的窗口对象 WindowWrap,只保存最近一段时间的数据,新增的时间窗口会覆盖最早的时间窗口;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">abstract</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">LeapArray</span><T> {

    <span style="color:#008000">//省略其他代码</span>

    <span style="color:#008000">//单个窗口的长度(1个窗口多长时间)</span>
    <span style="color:#0000ff">protected</span> <span style="color:#a31515">int</span> windowLengthInMs;
    <span style="color:#008000">//采样窗口个数</span>
    <span style="color:#0000ff">protected</span> <span style="color:#a31515">int</span> sampleCount;
    <span style="color:#008000">//全部窗口的长度(全部窗口多长时间)</span>
    <span style="color:#0000ff">protected</span> <span style="color:#a31515">int</span> intervalInMs;
    <span style="color:#0000ff">private</span> <span style="color:#a31515">double</span> intervalInSecond;
    <span style="color:#008000">//窗口数组:存储所有窗口(支持原子读取和写入)</span>
    <span style="color:#0000ff">protected</span> <span style="color:#0000ff">final</span> AtomicReferenceArray<WindowWrap<T>> array;
    <span style="color:#008000">//更新窗口数据时用的锁</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">final</span> <span style="color:#a31515">ReentrantLock</span> <span style="color:#008000">updateLock</span> <span style="color:#ab5656">=</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">ReentrantLock</span>();

    <span style="color:#0000ff">public</span> <span style="color:#a31515">LeapArray</span>(<span style="color:#a31515">int</span> sampleCount, <span style="color:#a31515">int</span> intervalInMs) {
        <span style="color:#008000">//计算单个窗口的长度</span>
        <span style="color:#0000ff">this</span>.windowLengthInMs = intervalInMs / sampleCount;
        <span style="color:#0000ff">this</span>.intervalInMs = intervalInMs;
        <span style="color:#0000ff">this</span>.intervalInSecond = intervalInMs / <span style="color:#880000">1000.0</span>;
        <span style="color:#0000ff">this</span>.sampleCount = sampleCount;
        <span style="color:#0000ff">this</span>.array = <span style="color:#0000ff">new</span> <span style="color:#a31515">AtomicReferenceArray</span><>(sampleCount);
    }
    <span style="color:#008000">//【点进去 4.2.3】获取当前窗口</span>
    <span style="color:#0000ff">public</span> WindowWrap<T> <span style="color:#a31515">currentWindow</span>() {
        <span style="color:#008000">//这里参数是当前时间</span>
        <span style="color:#0000ff">return</span> currentWindow(TimeUtil.currentTimeMillis());
    }
    <span style="color:#008000">//获取指定时间的窗口</span>
    <span style="color:#0000ff">public</span> WindowWrap<T> <span style="color:#a31515">currentWindow</span>(<span style="color:#a31515">long</span> timeMillis) {
        <span style="color:#0000ff">if</span> (timeMillis < <span style="color:#880000">0</span>) {
            <span style="color:#0000ff">return</span> <span style="color:#a31515">null</span>;
        }
        <span style="color:#008000">// 计算数组下标</span>
        <span style="color:#a31515">int</span> <span style="color:#008000">idx</span> <span style="color:#ab5656">=</span> calculateTimeIdx(timeMillis);
        <span style="color:#008000">//计算当前请求对应的窗口开始时间</span>
        <span style="color:#a31515">long</span> <span style="color:#008000">windowStart</span> <span style="color:#ab5656">=</span> calculateWindowStart(timeMillis);

        <span style="color:#008000">/*
         * 从 array 中获取窗口。有 3 种情况:
         * (1) array 中窗口不在,创建一个 CAS 并写入 array;
         * (2) array 中窗口开始时间 = 当前窗口开始时间,直接返回;
         * (3) array 中窗口开始时间 < 当前窗口开始时间,表示 o1d 窗口已过期,重置窗口数据并返回;
         */</span>
        <span style="color:#0000ff">while</span> (<span style="color:#a31515">true</span>) {
            <span style="color:#008000">// 取窗口</span>
            WindowWrap<T> old = array.get(idx);
            <span style="color:#008000">//(1)窗口不在</span>
            <span style="color:#0000ff">if</span> (old == <span style="color:#a31515">null</span>) {
                <span style="color:#008000">//创建一个窗口</span>
                WindowWrap<T> window = <span style="color:#0000ff">new</span> <span style="color:#a31515">WindowWrap</span><T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                <span style="color:#008000">//CAS将窗口写进 array 中并返回(CAS 操作确保只初始化一次)</span>
                <span style="color:#0000ff">if</span> (array.compareAndSet(idx, <span style="color:#a31515">null</span>, window)) {
                    <span style="color:#0000ff">return</span> window;
                } <span style="color:#0000ff">else</span> {
                    <span style="color:#008000">//并发写失败,释放 CPU 资源,避免有线程长时间占用 CPU,一般下次来的时候 array 中有数据了会命中第2种情况;</span>
                    Thread.yield();
                }
            <span style="color:#008000">//(2)array 中窗口开始时间 = 当前窗口开始时间</span>
            } <span style="color:#0000ff">else</span> <span style="color:#0000ff">if</span> (windowStart == old.windowStart()) {
                <span style="color:#008000">//直接返回</span>
                <span style="color:#0000ff">return</span> old;
            <span style="color:#008000">//(3)array 中窗口开始时间 < 当前窗口开始时间    </span>
            } <span style="color:#0000ff">else</span> <span style="color:#0000ff">if</span> (windowStart > old.windowStart()) {
                <span style="color:#008000">//尝试获取更新锁</span>
                <span style="color:#0000ff">if</span> (updateLock.tryLock()) {
                    <span style="color:#0000ff">try</span> {
                        <span style="color:#008000">//拿到锁的线程才重置窗口</span>
                        <span style="color:#0000ff">return</span> resetWindowTo(old, windowStart);
                    } <span style="color:#0000ff">finally</span> {
                        <span style="color:#008000">//释放锁</span>
                        updateLock.unlock();
                    }
                } <span style="color:#0000ff">else</span> {
                    <span style="color:#008000">//并发加锁失败,释放 CPU 资源,避免有线程长时间占用 CPU,一般下次来的时候因为 old 对象时间更新了会命中第 2 种情况;</span>
                    Thread.yield();
                }
            <span style="color:#008000">//理论上不会出现    </span>
            } <span style="color:#0000ff">else</span> <span style="color:#0000ff">if</span> (windowStart < old.windowStart()) {
                <span style="color:#008000">// 正常情况不会进入该分支(机器时钟回拨等异常情况)</span>
                <span style="color:#0000ff">return</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">WindowWrap</span><T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }
    <span style="color:#008000">//计算索引</span>
    <span style="color:#0000ff">private</span> <span style="color:#a31515">int</span> <span style="color:#a31515">calculateTimeIdx</span>(<span style="color:#008000">/*@Valid*/</span> <span style="color:#a31515">long</span> timeMillis) {
        <span style="color:#008000">//timeId 降低时间精度</span>
        <span style="color:#a31515">long</span> <span style="color:#008000">timeId</span> <span style="color:#ab5656">=</span> timeMillis / windowLengthInMs;
        <span style="color:#008000">//计算当前索引,这样我们就可以将时间戳映射到 leap 数组</span>
        <span style="color:#0000ff">return</span> (<span style="color:#a31515">int</span>)(timeId % array.length());
    }
    <span style="color:#008000">//计算窗口开始时间</span>
    <span style="color:#0000ff">protected</span> <span style="color:#a31515">long</span> <span style="color:#a31515">calculateWindowStart</span>(<span style="color:#008000">/*@Valid*/</span> <span style="color:#a31515">long</span> timeMillis) {
        <span style="color:#0000ff">return</span> timeMillis - timeMillis % windowLengthInMs;
    }
}
</code></span></span>

4.2.3 WindowWrap 窗口包装类

  • WindowWrap 是一个窗口对象,它是一个包装类,包装的对象是 MetricBucket
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">WindowWrap</span><T> {
    <span style="color:#008000">//窗口长度,与 LeapArray 的 windowLengthInMs 一致</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">final</span> <span style="color:#a31515">long</span> windowLengthInMs;
    <span style="color:#008000">//窗口开始时间,其值是 windowLengthInMs 的整数倍</span>
    <span style="color:#0000ff">private</span> <span style="color:#a31515">long</span> windowStart;
    <span style="color:#008000">//窗口的数据,支持 MetricBucket 类型,存储统计数据</span>
    <span style="color:#0000ff">private</span> T value;

    <span style="color:#008000">//省略其他代码</span>
}
</code></span></span>

4.2.4 MetricBucket 指标桶

  • MetricBucket 类的定义如下,可以发现指标数据存在 LongAdder[] counters中;
  • LongAdder 是 JDK1.8 中新增的类,用于在高并发场景下代替AtomicLong,以用空间换时间的方式降低了 CAS 失败的概率,从而提高性能;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">public</span> <span style="color:#0000ff">class</span> <span style="color:#a31515">MetricBucket</span> {
    <span style="color:#008000">/**
     * 存储指标的计数器;
     * LongAdder 是线程安全的计数器
     * counters[0]  PASS 通过数;
     * counters[1]  BLOCK 拒绝数;
     * counters[2]  EXCEPTION 异常数;
     * counters[3]  SUCCESS 成功数;
     * counters[4]  RT 响应时长;
     * counters[5]  OCCUPIED_PASS 预分配通过数;
     **/</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">final</span> LongAdder[] counters;

    <span style="color:#008000">//最小 RT,默认值是 5000ms</span>
    <span style="color:#0000ff">private</span> <span style="color:#0000ff">volatile</span> <span style="color:#a31515">long</span> minRt;

    <span style="color:#008000">//构造中初始化</span>
    <span style="color:#0000ff">public</span> <span style="color:#a31515">MetricBucket</span>() {
        MetricEvent[] events = MetricEvent.values();
        <span style="color:#0000ff">this</span>.counters = <span style="color:#0000ff">new</span> <span style="color:#a31515">LongAdder</span>[events.length];
        <span style="color:#0000ff">for</span> (MetricEvent event : events) {
            counters[event.ordinal()] = <span style="color:#0000ff">new</span> <span style="color:#a31515">LongAdder</span>();
        }
        initMinRt();
    }

    <span style="color:#008000">//覆盖指标</span>
    <span style="color:#0000ff">public</span> MetricBucket <span style="color:#a31515">reset</span>(MetricBucket bucket) {
        <span style="color:#0000ff">for</span> (MetricEvent event : MetricEvent.values()) {
            counters[event.ordinal()].reset();
            counters[event.ordinal()].add(bucket.get(event));
        }
        initMinRt();
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">this</span>;
    }

    <span style="color:#0000ff">private</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">initMinRt</span>() {
        <span style="color:#0000ff">this</span>.minRt = SentinelConfig.statisticMaxRt();
    }

    <span style="color:#008000">//重置指标为0</span>
    <span style="color:#0000ff">public</span> MetricBucket <span style="color:#a31515">reset</span>() {
        <span style="color:#0000ff">for</span> (MetricEvent event : MetricEvent.values()) {
            counters[event.ordinal()].reset();
        }
        initMinRt();
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">this</span>;
    }
    <span style="color:#008000">//获取指标,从 counters 中返回</span>
    <span style="color:#0000ff">public</span> <span style="color:#a31515">long</span> <span style="color:#a31515">get</span>(MetricEvent event) {
        <span style="color:#0000ff">return</span> counters[event.ordinal()].sum();
    }
    <span style="color:#008000">//添加指标</span>
    <span style="color:#0000ff">public</span> MetricBucket <span style="color:#a31515">add</span>(MetricEvent event, <span style="color:#a31515">long</span> n) {
        counters[event.ordinal()].add(n);
        <span style="color:#0000ff">return</span> <span style="color:#0000ff">this</span>;
    }

    <span style="color:#0000ff">public</span> <span style="color:#a31515">long</span> <span style="color:#a31515">pass</span>() {
        <span style="color:#0000ff">return</span> get(MetricEvent.PASS);
    }

    <span style="color:#0000ff">public</span> <span style="color:#a31515">long</span> <span style="color:#a31515">block</span>() {
        <span style="color:#0000ff">return</span> get(MetricEvent.BLOCK);
    }

    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addPass</span>(<span style="color:#a31515">int</span> n) {
        add(MetricEvent.PASS, n);
    }

    <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">addBlock</span>(<span style="color:#a31515">int</span> n) {
        add(MetricEvent.BLOCK, n);
    }

    <span style="color:#008000">//省略其他代码</span>
}
</code></span></span>

4.2.5 各数据结构的依赖关系

各数据结构的 UML 图.png

结构示意图.png

4.2.6 LeapArray 统计数据的大致思路

  • 创建一个长度为 n 的数组,数组元素就是窗口,窗口包装了 1 个指标桶,桶中存放了该窗口时间范围中对应的请求统计数据;
  • 可以想象成一个环形数组在时间轴上向右滚动,请求到达时,会命中数组中的一个窗口,那么该请求的数据就会存到命中的这个窗口包含的指标桶中;
  • 当数组转满一圈时,会回到数组的开头,而此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。具体过程如下图:

LeapArray 统计数据的大致思路

5. 熔断槽实施服务熔断 DegradeSlot.entry()

  • 服务熔断是通过 DegradeSlot 来实现的,它会根据用户配置的熔断规则和系统运行时各个 Node 中的统计数据进行熔断判断;
  • 注意:熔断功能在 Sentinel-1.8.0 版本前后有较大变化;
  • 我们给 DegradeSlot.entry() 方法里的语句打上断点,运行到光标处;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">entry</span>(Context context, ResourceWrapper resourceWrapper, DefaultNode node, <span style="color:#a31515">int</span> count, <span style="color:#a31515">boolean</span> prioritized, Object... args) <span style="color:#0000ff">throws</span> Throwable {
    <span style="color:#008000">//【断点步入】熔断检查</span>
    performChecking(context, resourceWrapper);
    <span style="color:#008000">//调用下一个 Slot</span>
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
</code></span></span>
  • 进入 DegradeSlot.performChecking() 方法,其逻辑与源码如下:
    • 根据资源名称获取断路器;
    • 循环判断每个断路器;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">void</span> <span style="color:#a31515">performChecking</span>(Context context, ResourceWrapper r) <span style="color:#0000ff">throws</span> BlockException {
    <span style="color:#008000">//根据 resourceName 获取断路器</span>
    List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
    <span style="color:#0000ff">if</span> (circuitBreakers == <span style="color:#a31515">null</span> || circuitBreakers.isEmpty()) {
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#008000">//循环判断每个断路器</span>
    <span style="color:#0000ff">for</span> (CircuitBreaker cb : circuitBreakers) {
        <span style="color:#008000">//【点进去】尝试通过断路器</span>
        <span style="color:#0000ff">if</span> (!cb.tryPass(context)) {
            <span style="color:#0000ff">throw</span> <span style="color:#0000ff">new</span> <span style="color:#a31515">DegradeException</span>(cb.getRule().getLimitApp(), cb.getRule());
        }
    }
}
</code></span></span>

5.1 继续或取消熔断功能

  • 进入 AbstractCircuitBreaker.tryPass() 方法,当请求超时并且处于探测恢复(半开状态,HALF-OPEN 状态)失败时继续断路功能;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#a31515">boolean</span> <span style="color:#a31515">tryPass</span>(Context context) {
    <span style="color:#008000">//当前断路器状态为关闭</span>
    <span style="color:#0000ff">if</span> (currentState.get() == State.CLOSED) {
        <span style="color:#0000ff">return</span> <span style="color:#a31515">true</span>;
    }
    <span style="color:#0000ff">if</span> (currentState.get() == State.OPEN) {
        <span style="color:#008000">//【点进去】对于半开状态,我们尝试通过</span>
        <span style="color:#0000ff">return</span> retryTimeoutArrived() && fromOpenToHalfOpen(context);
    }
    <span style="color:#0000ff">return</span> <span style="color:#a31515">false</span>;
}
</code></span></span>
  • 进入 AbstractCircuitBreaker.fromOpenToHalfOpen() 方法,实现状态的变更;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">protected</span> <span style="color:#a31515">boolean</span> <span style="color:#a31515">fromOpenToHalfOpen</span>(Context context) {
    <span style="color:#008000">//尝试将状态从 OPEN 设置为 HALF_OPEN</span>
    <span style="color:#0000ff">if</span> (currentState.compareAndSet(State.OPEN, State.HALF_OPEN)) {
        <span style="color:#008000">//状态变化通知</span>
        notifyObservers(State.OPEN, State.HALF_OPEN, <span style="color:#a31515">null</span>);
        <span style="color:#a31515">Entry</span> <span style="color:#008000">entry</span> <span style="color:#ab5656">=</span> context.getCurEntry();
        <span style="color:#008000">//在 entry 添加一个 exitHandler  entry.exit() 时会调用</span>
        entry.whenTerminate(<span style="color:#0000ff">new</span> <span style="color:#a31515">BiConsumer</span><Context, Entry>() {
            <span style="color:#2b91af">@Override</span>
            <span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">accept</span>(Context context, Entry entry) {
                <span style="color:#008000">//如果有发生异常,重新将状态设置为OPEN 请求不同通过</span>
                <span style="color:#0000ff">if</span> (entry.getBlockError() != <span style="color:#a31515">null</span>) {
                    currentState.compareAndSet(State.HALF_OPEN, State.OPEN);
                    notifyObservers(State.HALF_OPEN, State.OPEN, <span style="color:#880000">1.0d</span>);
                }
            }
        });
        <span style="color:#008000">//此时状态已设置为HALF_OPEN正常通行</span>
        <span style="color:#0000ff">return</span> <span style="color:#a31515">true</span>;
    }
    <span style="color:#008000">//熔断</span>
    <span style="color:#0000ff">return</span> <span style="color:#a31515">false</span>;
}
</code></span></span>
  • 上述讲解了:状态从 OPEN 变为 HALF_OPEN,HALF_OPEN 变为 OPEN;
  • 但状态从 HALF_OPEN 变为 CLOSE 需要在正常执行完请求后,由 entry.exit() 调用 DegradeSlot.exit() 方法来改变状态;

5.2 请求失败,启动熔断

  • 状态从 HALF_OPEN 变为 CLOSE 的实现方法在 DegradeSlot.exit()
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">exit</span>(Context context, ResourceWrapper r, <span style="color:#a31515">int</span> count, Object... args) {
    <span style="color:#a31515">Entry</span> <span style="color:#008000">curEntry</span> <span style="color:#ab5656">=</span> context.getCurEntry();
    <span style="color:#008000">//无阻塞异常</span>
    <span style="color:#0000ff">if</span> (curEntry.getBlockError() != <span style="color:#a31515">null</span>) {
        fireExit(context, r, count, args);
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#008000">//通过资源名获取断路器</span>
    List<CircuitBreaker> circuitBreakers = DegradeRuleManager.getCircuitBreakers(r.getName());
    <span style="color:#008000">//没有配置断路器,则直接放行</span>
    <span style="color:#0000ff">if</span> (circuitBreakers == <span style="color:#a31515">null</span> || circuitBreakers.isEmpty()) {
        fireExit(context, r, count, args);
        <span style="color:#0000ff">return</span>;
    }

    <span style="color:#0000ff">if</span> (curEntry.getBlockError() == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">for</span> (CircuitBreaker circuitBreaker : circuitBreakers) {
            <span style="color:#008000">//【点进去】在请求完成时</span>
            circuitBreaker.onRequestComplete(context);
        }
    }
    fireExit(context, r, count, args);
}
</code></span></span>
  • 进入 ExceptionCircuitBreaker.onRequestComplete() 方法,其主要逻辑与源码如下:
    • 请求失败比例与总请求比例加 1,用于判断后续是否超过阈值;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#2b91af">@Override</span>
<span style="color:#0000ff">public</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">onRequestComplete</span>(Context context) {
    <span style="color:#a31515">Entry</span> <span style="color:#008000">entry</span> <span style="color:#ab5656">=</span> context.getCurEntry();
    <span style="color:#0000ff">if</span> (entry == <span style="color:#a31515">null</span>) {
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#a31515">Throwable</span> <span style="color:#008000">error</span> <span style="color:#ab5656">=</span> entry.getError();
    <span style="color:#008000">//简单错误计数器</span>
    <span style="color:#a31515">SimpleErrorCounter</span> <span style="color:#008000">counter</span> <span style="color:#ab5656">=</span> stat.currentWindow().value();
    <span style="color:#0000ff">if</span> (error != <span style="color:#a31515">null</span>) {
        <span style="color:#008000">//异常请求数加 1</span>
        counter.getErrorCount().add(<span style="color:#880000">1</span>);
    }
    <span style="color:#008000">//总请求数加 1</span>
    counter.getTotalCount().add(<span style="color:#880000">1</span>);
    <span style="color:#008000">//【点进去】超过阈值时变更状态</span>
    handleStateChangeWhenThresholdExceeded(error);
}
</code></span></span>
  • 进入 ExceptionCircuitBreaker.handleStateChangeWhenThresholdExceeded() 方法,变更状态;
<span style="color:#333333"><span style="background-color:#ffffff"><code class="language-java"><span style="color:#0000ff">private</span> <span style="color:#0000ff">void</span> <span style="color:#a31515">handleStateChangeWhenThresholdExceeded</span>(Throwable error) {
    <span style="color:#008000">//全开则直接放行</span>
    <span style="color:#0000ff">if</span> (currentState.get() == State.OPEN) {
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#008000">//半开状态</span>
    <span style="color:#0000ff">if</span> (currentState.get() == State.HALF_OPEN) {
        <span style="color:#008000">//检查请求</span>
        <span style="color:#0000ff">if</span> (error == <span style="color:#a31515">null</span>) {
            <span style="color:#008000">//发生异常,将状态从半开 HALF_OPEN 转为关闭 CLOSE</span>
            fromHalfOpenToClose();
        } <span style="color:#0000ff">else</span> {
            <span style="color:#008000">//无异常,解开半开状态</span>
            fromHalfOpenToOpen(<span style="color:#880000">1.0d</span>);
        }
        <span style="color:#0000ff">return</span>;
    }
    
    <span style="color:#008000">//计算是否超过阈值</span>
    List<SimpleErrorCounter> counters = stat.values();
    <span style="color:#a31515">long</span> <span style="color:#008000">errCount</span> <span style="color:#ab5656">=</span> <span style="color:#880000">0</span>;
    <span style="color:#a31515">long</span> <span style="color:#008000">totalCount</span> <span style="color:#ab5656">=</span> <span style="color:#880000">0</span>;
    <span style="color:#0000ff">for</span> (SimpleErrorCounter counter : counters) {
        errCount += counter.errorCount.sum();
        totalCount += counter.totalCount.sum();
    }
    <span style="color:#0000ff">if</span> (totalCount < minRequestAmount) {
        <span style="color:#0000ff">return</span>;
    }
    <span style="color:#a31515">double</span> <span style="color:#008000">curCount</span> <span style="color:#ab5656">=</span> errCount;
    <span style="color:#0000ff">if</span> (strategy == DEGRADE_GRADE_EXCEPTION_RATIO) {
        <span style="color:#008000">//熔断策略为:异常比例</span>
        curCount = errCount * <span style="color:#880000">1.0d</span> / totalCount;
    }
    <span style="color:#0000ff">if</span> (curCount > threshold) {
        transformToOpen(curCount);
    }
}
</code></span></span>

6. Sentinel 源码结构图小结

  • SphU.entry():核心逻辑的入口函数;
    • CtSph.entryWithPriority():获取 Slot 链,操作 Slot 槽;
      • CtSph.lookProcessChain():获取 ProcessorSlot 链;
        • DefaultSlotChainBuilder.build():构造 DefaultProcessorSlotChain 链(里面有 10 个 Slot 插槽);
      • ProcessorSlot.entry():遍历 ProcessorSlot 链;
        • FlowSlot.entry():遍历到 FlowSlot 槽,限流规则;

          • FlowSlot.checkFlow():检查流量规则;
            • FlowRuleChecker.checkFlow():使用检查器检查流量规则;
              • FlowSlot.ruleProvider.apply():获取流控规则;
              • FlowRuleChecker.canPassCheck():校验每条规则;
                • FlowRuleChecker.passClusterCheck():集群模式;
                • FlowRuleChecker.passLocalCheck():单机模式;
                  • FlowRuleChecker.selectNodeByRequesterAndStrategy():获取 Node;
                  • FlowRule.getRater():获得流控行为 TrafficShapingController;
                  • TrafficShapingController.canPass():执行流控行为;
        • StatisticSlot.entry:遍历到 StatisticSlot 槽,统计数据;

          • DefaultNode.increaseThreadNum():统计“增加线程数”;
            • StatisticNode.increaseThreadNum():统计“请求通过数”;
              • ArrayMetric.ArrayMetric():初始化指标数组;
                • LeapArray:环形数组;
                  • WindowWrap:窗口包装类;
                • MetricBucket:指标桶;
          • DefaultNode.addPassRequest():统计“增加线程数”;
            • StatisticNode.addPassRequest():同上;
        • DegradeSlot.entry():遍历到 DegradeSlot 槽,服务熔断;

          • DegradeSlot.performChecking():执行检查;
            • DegradeRuleManager.getCircuitBreakers():根据 resourceName 获取断路器;
            • AbstractCircuitBreaker.tryPass():继续或取消熔断功能;
              • AbstractCircuitBreaker.fromOpenToHalfOpen():尝试通过半开状态;
        • DegradeSlot.exit():请求失败(超时),启动熔断;

          • ExceptionCircuitBreaker.onRequestComplete():在请求完成时操作;
            • ExceptionCircuitBreaker.handleStateChangeWhenThresholdExceeded():变更状态;

最后

新人制作,如有错误,欢迎指出,感激不尽!欢迎关注公众号,会分享一些更日常的东西!

如需转载,请标注出处!

来源:https://www.cnblogs.com/dlhjw/p/15858198.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值