This commit is contained in:
by931
2022-09-06 22:30:37 +08:00
parent 66970f3e38
commit 3d6528675a
796 changed files with 3382 additions and 3382 deletions

View File

@@ -194,7 +194,7 @@ function hide_canvas() {
}
</code></pre>
<p>如上面代码所示Bucket 记录一段时间内的各项指标数据用的是一个 LongAdder 数组LongAdder 保证了数据修改的原子性,并且性能比 AtomicInteger 表现更好。数组的每个元素分别记录一个时间窗口内的请求总数、异常数、总耗时,如下图所示。</p>
<p><img src="assets/1c69e7b0-e0b0-11ea-b45a-99f77a1eea3e" alt="04-01-counters" /></p>
<p><img src="assets/1c69e7b0-e0b0-11ea-b45a-99f77a1eea3e" alt="png" /></p>
<p>Sentinel 用枚举类型 MetricEvent 的 ordinal 属性作为下标ordinal 的值从 0 开始,按枚举元素的顺序递增,正好可以用作数组的下标。在 qps-helper 中LongAdder 被替换为 j.u.c 包下的 atomic 类了,并且只保留 EXCEPTION、SUCCESS、RT代码如下。</p>
<pre><code class="language-java">// 事件类型
public enum MetricEvent {
@@ -220,11 +220,11 @@ public void add(MetricEvent event, long n) {
<h3>滑动窗口</h3>
<p>如果我们希望能够知道某个接口的每秒处理成功请求数(成功 QPS、每秒处理失败请求数失败 QPS以及处理每个成功请求的平均耗时avg RT我们只需要控制 Bucket 统计一秒钟的指标数据即可。我们只需要控制 Bucket 统计一秒钟内的指标数据即可。但如何才能确保 Bucket 存储的就是精确到 1 秒内的数据呢?</p>
<p>最 low 的做法就是启一个定时任务每秒创建一个 Bucket但统计出来的数据误差绝对很大。Sentinel 是这样实现的,它定义一个 Bucket 数组,根据时间戳来定位到数组的下标。假设我们需要统计每 1 秒处理的请求数等数据,且只需要保存最近一分钟的数据。那么 Bucket 数组的大小就可以设置为 60每个 Bucket 的 windowLengthInMs窗口时间大小就是 1000 毫秒1 秒),如下图所示。</p>
<p><img src="assets/3968d920-e0b0-11ea-8f3f-a12c78e9865c" alt="04-02-bucket" /></p>
<p><img src="assets/3968d920-e0b0-11ea-8f3f-a12c78e9865c" alt="png" /></p>
<p>由于每个 Bucket 存储的是 1 秒的数据,假设 Bucket 数组的大小是无限大的,那么我们只需要将当前时间戳去掉毫秒部分就能得到当前的秒数,将得到的秒数作为索引就能从 Bucket 数组中获取当前时间窗口的 Bucket。</p>
<p>一切资源均有限,所以我们不可能无限的存储 Bucket我们也不需要存储那么多历史数据在内存中。当我们只需要保留一分钟的数据时Bucket 数组的大小就可以设置为 60我们希望这个数组可以循环使用并且永远只保存最近 1 分钟的数据,这样不仅可以避免频繁的创建 Bucket也减少内存资源的占用。</p>
<p>这种情况下如何定位 Bucket 呢?我们只需要将当前时间戳去掉毫秒部分得到当前的秒数,再将得到的秒数与数组长度取余数,就能得到当前时间窗口的 Bucket 在数组中的位置(索引),如下图所示:</p>
<p><img src="assets/4223a680-e0b0-11ea-9345-e7c4c4dd55f7" alt="04-03-window" /></p>
<p><img src="assets/4223a680-e0b0-11ea-9345-e7c4c4dd55f7" alt="png" /></p>
<p>根据当前时间戳计算出当前时间窗口的 Bucket 在数组中的索引,算法实现如下:</p>
<pre><code class="language-java"> private int calculateTimeIdx(long timeMillis) {
/**
@@ -355,7 +355,7 @@ public void add(MetricEvent event, long n) {
<h3>获取当前时间戳的前一个 Bucket</h3>
<p>根据当前时间戳计算出当前 Bucket 的时间窗口开始时间,用当前 Bucket 的时间窗口开始时间减去一个窗口时间大小就能定位出前一个 Bucket。</p>
<p>由于是使用数组实现滑动窗口,数组的每个元素都会被循环使用,因此当前 Bucket 与前一个 Bucket 会有相差一个完整的滑动窗口周期的可能,如下图所示。</p>
<p><img src="assets/638d4510-e0b0-11ea-b1dc-4f04d04fa39d" alt="04-04-pre-cut-ptr" /></p>
<p><img src="assets/638d4510-e0b0-11ea-b1dc-4f04d04fa39d" alt="png" /></p>
<p>当前时间戳对应的 Bucket 的时间窗口开始时间戳为 1595974702000而前一个 Bucket 的时间窗口开始时间戳可能是 1595974701000也可能是一个滑动窗口周期之前的 1595974641000。所以在获取到当前 Bucket 的前一个 Bucket 时,需要根据 Bucket 的时间窗口开始时间与当前时间戳比较,如果跨了一个周期就是无效的。</p>
<h3>总结</h3>
<ul>

View File

@@ -254,7 +254,7 @@ function hide_canvas() {
}
</code></pre>
<p>它的几个实现类DefaultNode、ClusterNode、EntranceNode、StatisticNode 的关系如下图所示。</p>
<p><img src="assets/e3082850-e0b0-11ea-860c-27aeccb4bc5b" alt="05-00-nodes" /></p>
<p><img src="assets/e3082850-e0b0-11ea-860c-27aeccb4bc5b" alt="png" /></p>
<h4><strong>StatisticNode</strong></h4>
<p>Statistic 即统计的意思StatisticNode 是 Node 接口的实现类,是实现实时指标数据统计 Node。</p>
<pre><code class="language-java">public class StatisticNode implements Node {
@@ -384,7 +384,7 @@ try {
}
</code></pre>
<p>EntranceNode、DefaultNode、ClusterNode 与滑动窗口的关系如下图所示:</p>
<p><img src="assets/7b4516a0-e0b1-11ea-8111-1feecf2da711" alt="05-01-node" /></p>
<p><img src="assets/7b4516a0-e0b1-11ea-8111-1feecf2da711" alt="png" /></p>
<h3>Sentinel 中的 Context 与 Entry</h3>
<p>理解 Context 与 Entry 也是理解 Sentinel 整个工作流程的关键,其中 Entry 还会涉及到“调用树”这一概念。</p>
<h4><strong>Context</strong></h4>
@@ -442,12 +442,12 @@ try {
</code></pre>
<p>CtEntry 用于维护父子 Entry每一次调用 SphU#entry 方法都会创建一个 CtEntry。如果服务 B 在处理一个请求的路径上会多次调用 SphU#entry那么这些 CtEntry 会构成一个双向链表。在每次创建 CtEntry都会将 Context.curEntry 设置为这个新的 CtEntry双向链表的作用就是在调用 CtEntry#exit 方法时,能够将 Context.curEntry 还原为上一个资源的 CtEntry。</p>
<p>例如,在服务 B 接收到服务 A 的请求时,会调用 SphU#entry 方法创建一个 CtEntry我们取个代号 ctEntry1此时的 ctEntry1 的父节点parent为空。当服务 B 向服务 C 发起调用时OpenFeign 适配器调用 SphU#entry 的方法会创建一个 CtEntry我们取个代号 ctEntry2此时 ctEntry2 的父节点parent就是 ctEntry1ctEntry1 的子节点child就是 ctEntry2如下图所示。</p>
<p><img src="assets/d93083d0-e0b1-11ea-b45a-99f77a1eea3e" alt="05-02-ctentry" /></p>
<p><img src="assets/d93083d0-e0b1-11ea-b45a-99f77a1eea3e" alt="png" /></p>
<h4><strong>ROOT 与调用树</strong></h4>
<p>Constants 常量类用于声明全局静态常量Constants 有一个 ROOT 静态字段,类型为 EntranceNode。</p>
<p>在调用 ContextUtil#enter 方法时,如果还没有为当前入口创建 EntranceNode则会为当前入口创建 EntranceNode将其赋值给 Context.entranceNode同时也会将这个 EntranceNode 添加到 Constants.ROOT 的子节点childList。资源对应的 DefaultNode 则是在 NodeSelectorSlot 中创建,并赋值给 Context.curEntry.curNode。</p>
<p>Constants.ROOT、Context.entranceNode 与 Entry.curNode 三者关系如下图所示。</p>
<p><img src="assets/e5343aa0-e0b1-11ea-8111-1feecf2da711" alt="05-03-root-context-resource" /></p>
<p><img src="assets/e5343aa0-e0b1-11ea-8111-1feecf2da711" alt="png" /></p>
<h3>Sentinel 中的 ProcessorSlot</h3>
<p>ProcessorSlot 直译就是处理器插槽,是 Sentinel 实现限流降级、熔断降级、系统自适应降级等功能的切入点。Sentinel 提供的 ProcessorSlot 可以分为两类,一类是辅助完成资源指标数据统计的切入点,一类是实现降级功能的切入点。</p>
<p>辅助资源指标数据统计的 ProcessorSlot</p>

View File

@@ -397,7 +397,7 @@ try {
<p>Sentinel 的核心骨架是 ProcessorSlotChain所以核心的流程是一次 SphU#entry 方法的调用以及一次 CtEntry#exit 方法的调用。</p>
<p>SphU#entry 方法调用 CtSph#entry 方法CtSph 负责为资源创建 ResourceWrapper 对象并为资源构造一个全局唯一的 ProcessorSlotChain、为资源创建 CtEntry 并将 CtEntry 赋值给当前调用链路的 Context.curEntry、最后调用 ProcessorSlotChain#entry 方法完成一次单向链表的 entry 方法调用。</p>
<p>ProcessorSlotChain 的一次 entry 方法的调用过程如下图所示。</p>
<p><img src="assets/43a296a0-e0b6-11ea-bf6b-993ba43d1a8c" alt="06-01-chian" /></p>
<p><img src="assets/43a296a0-e0b6-11ea-bf6b-993ba43d1a8c" alt="png" /></p>
<h4><strong>调用 Tracer 的 trace 方法</strong></h4>
<p>只在抛出非 BlockException 异常时才会调用 Tracer#trace 方法,用于记录当前资源调用异常,为当前资源的 DefaultNode 自增异常数。</p>
<pre><code class="language-java">public class Tracer {

View File

@@ -231,9 +231,9 @@ public class WebMvcTestController {
}
</code></pre>
<p>我们不需要添加任何规则,只是为了调试 Sentinel 的源码。将 demo 启动起来后,在浏览器访问“/hello”接口在 NodeSelectorSlot#entry 方法的绑定调用树这一行代码下断点,观察此时 Context 的字段信息。正常情况下我们可以看到如下图所示的结果。</p>
<p><img src="assets/474e2790-e0b8-11ea-b6a2-55d68b770f3d" alt="08-01-one01" /></p>
<p><img src="assets/474e2790-e0b8-11ea-b6a2-55d68b770f3d" alt="png" /></p>
<p>从上图中可以看出,此时的 Context.entranceNode 的子节点为空childList 的大小为 0并且当前 CtEntry 父、子节点都是 NullcurEntry 字段。当绑定调用树这一行代码执行完成后Context 的字段信息如下图所示:</p>
<p><img src="assets/529237e0-e0b8-11ea-b45a-99f77a1eea3e" alt="08-02-one02" /></p>
<p><img src="assets/529237e0-e0b8-11ea-b45a-99f77a1eea3e" alt="png" /></p>
<p>从上图可以看出NodeSelectorSlot 为当前资源创建的 DefaultNode 被添加到了 Context.entranceNode 的子节点。entranceNode 类型为 EntranceNode在调用 ContextUtil#enter 方法时创建在第一次创建名为“sentinel_spring_web_context”的 Context 时创建,相同名称的 Context 都使用同一个 EntranceNode。并且该 EntranceNode 在创建时会被添加到 Constant.ROOT。</p>
<p>此时Constant.ROOT、Context.entranceNode、当前访问资源的 DefaultNode 构造成的调用树如下:</p>
<pre><code class="language-java"> ROOT (machine-root)
@@ -282,7 +282,7 @@ public class WebMvcTestController {
</code></pre>
<p>我们可将 doBusiness 方法看成是远程调用例如调用第三方的接口接口名称为“http://wujiuye.com/hello2”使用 POST 方式调用那么我们可以使用“POST:http://wujiuye.com/hello2”作为资源名称并将流量类型设置为 OUT 类型。上下文名称取名为&quot;my_context&quot;</p>
<p>现在启动 demo使用浏览器访问“/hello”接口。当代码执行到 apiHello 方法时,在 NodeSelectorSlot#entry 方法的绑定调用树这一行代码下断点。当绑定调用树这行代码执行完成后Context 的字段信息如下图所示。</p>
<p><img src="assets/7e9fd630-e0b8-11ea-8111-1feecf2da711" alt="08-03-two01" /></p>
<p><img src="assets/7e9fd630-e0b8-11ea-8111-1feecf2da711" alt="png" /></p>
<p>如图所示Sentinel 并没有创建名称为 my_context 的 Context还是使用应用接收到请求时创建名为“sentinel_spring_web_context”的 Context所以处理浏览器发送过来的请求的“GET:/hello”资源是本次调用链路的入口资源Sentinel 在调用链路入口处创建 Context 之后不再创建新的 Context。</p>
<p>由于之前并没有为名称为“POST:http://wujiuye.com/hello2”的资源创建 ProcessorSlotChain所以 SphU#entry 会为该资源创建一个 ProcessorSlotChain也就会为该 ProcessorSlotChain 创建一个 NodeSelectorSlot。在执行到 NodeSelectorSlot#entry 方法时,就会为该资源创建一个 DefaultNode而将该资源的 DefaultNode 绑定到节点树后,该资源的 DefaultNode 就会成为“GET:/hello”资源的 DefaultNode 的子节点,调用树如下。</p>
<pre><code class="language-java"> ROOT (machine-root)
@@ -294,7 +294,7 @@ public class WebMvcTestController {
DefaultNode (POST:/hello2)
</code></pre>
<p>此时,当前调用链路上也已经存在两个 CtEntry这两个 CtEntry 构造一个双向链表,如下图所示。</p>
<p><img src="assets/91d06760-e0b8-11ea-9345-e7c4c4dd55f7" alt="08-04-two02" /></p>
<p><img src="assets/91d06760-e0b8-11ea-9345-e7c4c4dd55f7" alt="png" /></p>
<p>虽然存在两个 CtEntry但此时 Context.curEntry 指向第二个 CtEntry第二个 CtEntry 在 apiHello 方法中调用 SphU#entry 方法时创建,当执行完 doBusiness 方法后,调用当前 CtEntry#exit 方法,由该 CtEntry 将 Context.curEntry 还原为该 CtEntry 的父 CtEntry。这有点像入栈和出栈操作例如栈帧在 Java 虚拟机栈的入栈和出栈,调用方法时方法的栈帧入栈,方法执行完成栈帧出栈。</p>
<p>NodeSelectorSlot#entry 方法我们还有一行代码没有分析,就是将当前创建的 DefaultNode 设置为 Context 的当前节点,代码如下:</p>
<pre><code class="language-java">// 替换 Context.curNode 为当前 DefaultNode
@@ -359,7 +359,7 @@ context.setCurNode(node);
</code></pre>
<p>ClusterBuilderSlot 使用一个 Map 缓存资源的 ClusterNode并且用一个非静态的字段维护当前资源的 ClusterNode。因为一个资源只会创建一个 ProcessorSlotChain意味着 ClusterBuilderSlot 也只会创建一个,那么让 ClusterBuilderSlot 持有该资源的 ClusterNode 就可以省去每次都从 Map 中获取的步骤,这当然也是 Sentinel 为性能做出的努力。</p>
<p>ClusterBuilderSlot#entry 方法的 node 参数由前一个 ProcessorSlot 传递过来,也就是 NodeSelectorSlot 传递过来的 DefaultNode。ClusterBuilderSlot 将 ClusterNode 赋值给 DefaultNode.clusterNode那么后续的 ProcessorSlot 就能从 node 参数中取得 ClusterNode。DefaultNode 与 ClusterNode 的关系如下图所示。</p>
<p><img src="assets/c90597e0-e0b9-11ea-87f8-01fab3b387f8" alt="08-05-default-cluster" /></p>
<p><img src="assets/c90597e0-e0b9-11ea-87f8-01fab3b387f8" alt="png" /></p>
<p>ClusterNode 有一个 Map 类型的字段用来缓存 origin 与 StatisticNode 的映射,代码如下:</p>
<pre><code class="language-java">public class ClusterNode extends StatisticNode {
private final String name;

View File

@@ -359,7 +359,7 @@ function hide_canvas() {
}
</code></pre>
<p>rollingCounterInSecond 是一个秒级的滑动窗口rollingCounterInMinute 是一个分钟级的滑动窗口,类型为 ArrayMetric。分钟级的滑动窗口一共有 60 个 MetricBucket每个 MetricBucket 都被 WindowWrap 包装,每个 MetricBucket 统计一秒钟内的各项指标数据,如下图所示:</p>
<p><img src="assets/b0e48b80-e12c-11ea-8111-1feecf2da711" alt="09-02-leaparray" /></p>
<p><img src="assets/b0e48b80-e12c-11ea-8111-1feecf2da711" alt="png" /></p>
<p>当调用 rollingCounterInMinute#addSuccess 方法时,由 ArrayMetric 根据当前时间戳获取当前时间窗口的 MetricBucket再调用 MetricBucket#addSuccess 方法将 success 这项指标的值加上方法参数传递进来的值(一般是 1。MetricBucket 使用 LongAdder 记录各项指标数据的值。</p>
<p>Sentinel 在 MetricEvent 枚举类中定义了 Sentinel 会收集哪些指标数据MetricEvent 枚举类的源码如下:</p>
<pre><code class="language-java">public enum MetricEvent {

View File

@@ -206,7 +206,7 @@ function hide_canvas() {
<li>limitApp只对哪个或者哪些调用来源生效若为 default 则不区分调用来源。</li>
</ul>
<p>Rule、AbstractRule 与其它实现类的关系如下图所示:</p>
<p><img src="assets/7ed0f0f0-e0bb-11ea-b6a2-55d68b770f3d" alt="10-01-rule" /></p>
<p><img src="assets/7ed0f0f0-e0bb-11ea-b6a2-55d68b770f3d" alt="png" /></p>
<p>FlowRule 是限流规则配置类FlowRule 继承 AbstractRule 并实现 Rule 接口。FlowRule 源码如下,非完整源码,与实现集群限流相关的字段暂时去掉了。</p>
<pre><code class="language-java">public class FlowRule extends AbstractRule {
// 限流阈值类型 qps|threads

View File

@@ -191,7 +191,7 @@ function hide_canvas() {
</code></pre>
<p>Sentinel 冷启动限流算法参考了 Guava 的 SmoothRateLimiter 实现的冷启动限流算法但实现上有很大的区别Sentinel 主要用于控制每秒的 QPS不会控制每个请求的间隔时间只要满足每秒通过的 QPS 即可。正因为与 Guava 的不同,官方文档目前也没有很详细的介绍具体实现,单看源码很难揣摩作者的思路,加上笔者水平有限,没能切底理解 Sentinel 冷启动限流算法实现的细节,因此我们也不过深的去分析 WarmUpController 的源码,只是结合 Guava 的实现算法作个简单介绍。</p>
<p>Guava 的 SmoothRateLimiter 基于 Token Bucket 算法实现冷启动。我们先看一张图,从而了解 SmoothRateLimiter 中的一些基础知识。</p>
<p><img src="assets/d7bf7130-e0bd-11ea-b45a-99f77a1eea3e" alt="12-01-warmup01" /></p>
<p><img src="assets/d7bf7130-e0bd-11ea-b45a-99f77a1eea3e" alt="png" /></p>
<ul>
<li>横坐标 storedPermits 代表存储桶中的令牌数量。</li>
<li>纵坐标代表获取一个令牌需要的时间,即请求通过的时间间隔。</li>
@@ -336,7 +336,7 @@ long aboveToken = restToken - warningToken;
double warningQps = Math.nextUp(1.0 / (aboveToken * slope + 1.0 / count));
</code></pre>
<p>我们看图理解这个公式。</p>
<p><img src="assets/884d8050-e0be-11ea-9345-e7c4c4dd55f7" alt="12-01-warmup02" /></p>
<p><img src="assets/884d8050-e0be-11ea-9345-e7c4c4dd55f7" alt="png" /></p>
<p>结合上图我们可以看出:</p>
<ul>
<li>图中的 x1 虚线的长度就等于 aboveToken。</li>

View File

@@ -203,7 +203,7 @@ function hide_canvas() {
<h4><strong>熔断降级判断流程</strong></h4>
<p>DegradeSlot 是实现熔断降级的切入点,它作为 ProcessorSlot 插入到 ProcessorSlotChain 链表中,在 entry 方法中调用 Checker 去判断是否熔断当前请求,如果熔断则抛出 Block 异常。</p>
<p>Checker 并不是一个接口,而是一种检测行为,限流的 ckeck 由 FlowRuleChecker 实现,而熔断的 check 行为则由 DegradeRuleManager 负责,真正 check 逻辑判断由 DegradeRule 实现,流程如下图所示。</p>
<p><img src="assets/e1369f60-e12e-11ea-b6a2-55d68b770f3d" alt="13-01-degradeslot" /></p>
<p><img src="assets/e1369f60-e12e-11ea-b6a2-55d68b770f3d" alt="png" /></p>
<p>当 DegradeSlot#entry 方法被调用时,由 DegradeSlot 调用 DegradeRuleManager#checkDegrade 方法检查当前请求是否满足某个熔断降级规则。熔断规则配置由 DegradeRuleManager 加载,所以 DegradeSlot 将 check 逻辑交给 DegradeRuleManager 去完成checkDegrade 方法的源码如下:</p>
<pre><code class="language-java">public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count)
throws BlockException {
@@ -365,7 +365,7 @@ public long totalException() {
</code></pre>
<h4><strong>系统自适应限流判断流程</strong></h4>
<p>当 SystemSlot#entry 方法被调用时,由 SystemSlot 调用 SystemRuleManager#checkSystem 方法判断是否需要限流,流程如下图所示:</p>
<p><img src="assets/39b558c0-e12f-11ea-b6a2-55d68b770f3d" alt="13-03-systemslot" /></p>
<p><img src="assets/39b558c0-e12f-11ea-b6a2-55d68b770f3d" alt="png" /></p>
<p>SystemRuleManager#checkSystem 方法从全局的资源指标数据统计节点 Constans.ENTRY_NODE 读取当前时间窗口的指标数据,判断总的 QPS、平均耗时这些指标数据是否达到阈值或者总占用的线程数是否达到阈值如果达到阈值则抛出 Block 异常SystemBlockException。除此之外checkSystem 方法还实现了根据系统当前 Load 和 CPU 使用率限流。</p>
<p>SystemRuleManager#checkSystem 方法源码如下:</p>
<pre><code class="language-java">public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
@@ -414,7 +414,7 @@ public long totalException() {
</ul>
<h4><strong>获取系统负载和 CPU 使用率</strong></h4>
<p>使用 TOP 命令可查看系统的平均负载Load和 CPU 使用率,如下图所示:</p>
<p><img src="assets/4eecdd80-e12f-11ea-860c-27aeccb4bc5b" alt="13-02-system-top" /></p>
<p><img src="assets/4eecdd80-e12f-11ea-860c-27aeccb4bc5b" alt="png" /></p>
<ul>
<li>Load Avg三个浮点数分别代表 1 分钟、5 分钟、15 分钟内系统的平均负载。</li>
<li>CPUusage 为 CPU 总的使用率user 为用户线程的 CPU 使用率sys 为系统线程的 CPU 使用率。</li>

View File

@@ -176,7 +176,7 @@ function hide_canvas() {
<div><h1>17 Sentinel 主流框架适配</h1>
<p>使用 Sentinel 需要用 try-catch-finally 将需要保护的资源(方法或者代码块)包装起来,在目标方法或者代码块执行之前,调用 ContextUtil#enter 方法以及 SphU#entry 方法,在抛出异常时,如果非 BlockException 异常需要调用 Tracer#trace 记录异常,修改异常指标数据,在 finally 中需要调用 Entry#exit 方法,以及 ContextUtil#exit 方法。</p>
<p>为了节省这些步骤Sentinel 提供了对主流框架的适配,如适配 Spring MVC、Webflux、Dubbo、Api Gateway 等框架。当然,对于 Sentinel 未适配的框架,我们也可以自己实现适配器。在 Sentinel 源码之外alibaba 的 spring-cloud-starter-alibaba-sentinel 也为 Sentinel 提供与 OpenFeign 框架整合的支持。</p>
<p><img src="assets/ae715de0-ed3b-11ea-be9c-f7616f01fc23" alt="17-01-sentinel-adapter" /></p>
<p><img src="assets/ae715de0-ed3b-11ea-be9c-f7616f01fc23" alt="png" /></p>
<h3>Spring MVC 适配器</h3>
<p>Sentinel 借助 Spring MVC 框架的 HandlerInterceptor 适配 Spring MVC但也需要我们借助 WebMvcConfigurer 将 SentinelWebInterceptor 注册到 Spring MVC 框架。</p>
<h4><strong>使用步骤</strong></h4>
@@ -398,7 +398,7 @@ public interface DemoService {
<p>当满足熔断条件时Sentinel 会抛出一个 DegradeException 异常,如果配置了 fallback那么 Sentinel 会从 Bean 工厂中根据 fallback 属性配置的类型取一个 Bean 并调用接口方法。</p>
<h4><strong>Sentinel 与 OpenFeign 整合实现原理</strong></h4>
<p>当 Sentinel 与 OpenFeign、Ribbon 整合时,客户端向服务端发起一次请求的过程如下图所示。</p>
<p><img src="assets/7b545840-ed3b-11ea-9210-e5c7b119b96e" alt="17-02-openfeign-sentinel" /></p>
<p><img src="assets/7b545840-ed3b-11ea-9210-e5c7b119b96e" alt="png" /></p>
<ol>
<li>当调用@FeignClient 接口的方法时,由 Sentinel 提供的方法调用拦截器SentinelInvocationHandler拦截方法的执行根据接口方法上注解的 url 生成资源名称,然后调用 Sentinel 的 SphU#entry 方法(完成所有 ProcessorSlot#entry 方法的调用),判断当前发起的请求是否需要熔断;</li>
<li>非熔断降级情况下,继续将请求交给 OpenFeign 的 MethodHandler 处理;</li>
@@ -408,7 +408,7 @@ public interface DemoService {
</ol>
<p>可见Sentinel 处在接口调用的最前端,因此 Sentinel 统计的指标数据即不会受 Ribbon 的重试影响也不会受 OpenFeign 的重试影响。</p>
<p>Sentinel 通过自己提供 InvocationHandler 替换 OpenFeign 的 InvocationHandler 实现请求拦截。SentinelInvocationHandler 源码调试如下图所示。</p>
<p><img src="assets/d7b506c0-ed3b-11ea-816b-87f82de6664d" alt="17-02-openfeign-sentinel02" /></p>
<p><img src="assets/d7b506c0-ed3b-11ea-816b-87f82de6664d" alt="png" /></p>
<p>InvocationHandler 是 OpenFeign 为接口生成 JDK 动态代理类时所需要的是接口的方法拦截处理器Sentinel 通过替换 OpenFeign 的 InvocationHandler 拦截方法的执行,在 OpenFeign 处理接口调用之前完成熔断降级的检查。</p>
<p>那么Sentinel 是如何将原本的 FeignInvocationHandler 替换为 SentinelInvocationHandler 的呢?</p>
<p>OpenFeign 通过 Feign.Builder 类创建接口的代理类,所以 Sentinel 直接将 Feign.Builder 也替换成了 SentinelFeign.Builder由 SentinelFeignAutoConfiguration 自动配置类向 Spring 的 Bean 容器注入 SentinelFeign.Builder代码如下</p>

View File

@@ -190,7 +190,7 @@ function hide_canvas() {
<li>如果是本地限流,则调用流量效果控制器判断是否拒绝当前请求。</li>
</ol>
<p>实现集群限流是在第3步的基础上如果限流规则的 clusterMode 配置为集群限流模式,则向集群限流服务端发起远程调用,由集群限流服务端判断是否拒绝当前请求,流量效果控制在集群限流服务端完成。我们结合下图理解。</p>
<p><img src="assets/9d4a3bd0-ed3c-11ea-b07d-070a5bf07ad2" alt="18-01-cluster-flow-lct" /></p>
<p><img src="assets/9d4a3bd0-ed3c-11ea-b07d-070a5bf07ad2" alt="png" /></p>
<p>如图所示,当规则配置为集群限流模式时,通过 TokenService 向集群限流服务端发起请求,根据响应结果决定如何控制当前请求。</p>
<h3>集群限流服务端的两种模式</h3>
<p>Sentinel 支持两种模式启动集群限流服务端,分别是嵌入模式、独立模式,两种模式都有各种的优缺点。</p>

View File

@@ -230,7 +230,7 @@ function hide_canvas() {
</code></pre>
<p>EmbeddedClusterTokenServer 接口继承 ClusterTokenServer并继承 TokenService 接口,即整合客户端和服务端的功能,为嵌入式模式提供支持。在嵌入式模式下,如果当前节点是集群限流服务端,那就没有必要发起网络请求。</p>
<p>这些接口以及默认实现类的关系如下图所示。</p>
<p><img src="assets/a3a9f1c0-f5b9-11ea-a625-2d171281165b" alt="19-01-classs" /></p>
<p><img src="assets/a3a9f1c0-f5b9-11ea-a625-2d171281165b" alt="png" /></p>
<p>其中 DefaultClusterTokenClient 是 sentinel-cluster-client-default 模块中的 ClusterTokenClient 接口实现类DefaultTokenService 与 DefaultEmbeddedTokenServer 分别是 sentinel-cluster-server-default 模块中的 ClusterTokenServer 接口与 EmbeddedClusterTokenServer 接口的实现类。</p>
<p>当使用嵌入模式启用集群限流服务端时,使用的是 EmbeddedClusterTokenServer否则使用 ClusterTokenServer通过 Java SPI 实现。</p>
<h4><strong>集群限流客户端</strong></h4>

View File

@@ -175,9 +175,9 @@ function hide_canvas() {
<p id="tip" align="center"></p>
<div><h1>21 番外篇Sentinel 1.8.0 熔断降级新特性解读</h1>
<p>在本专栏写作完成之际,笔者看到阿里官方推出了 Sentinel 1.8.0 版本,该版本发布时间为 2020/08/20。</p>
<p><img src="assets/dcf8dfe0-f5cd-11ea-949f-999a932fc96d" alt="21-01-version" /></p>
<p><img src="assets/dcf8dfe0-f5cd-11ea-949f-999a932fc96d" alt="png" /></p>
<p>与此同时,官方文档也更新了关于熔断降级的介绍。</p>
<p><img src="assets/08854040-f5ce-11ea-a7f1-a7929799274d" alt="21-02-wiki" /></p>
<p><img src="assets/08854040-f5ce-11ea-a7f1-a7929799274d" alt="png" /></p>
<p>从官方文档来看,该版本的最大亮点是对熔断降级功能进行了重构。旧版本的熔断降级功能对慢调用并不友好,而新版本改善了这个问题。</p>
<p>在微服务项目中,一个微服务常常需要调用内部其它服务的接口,即便是单体架构项目,也免不了会调用一些第三方 API、访问数据库使用熔断降级功能可以有效避免外部因素导致服务自身不可用甚至进程挂掉的情况发生。也许我们项目并不需要限流但熔断机制却是微服务项目不可或缺的降级方式。所以笔者决定为专栏添加一篇文章介绍新版本的熔断降级。</p>
<p>笔者在得知 Sentinel 更新 1.8.0 版本的第一时间就看了熔断降级重构后的源码,从源码对比新旧版本细节上的差异。</p>