乐趣区

关于云计算:Spring-Cloud-Gateway的断路器CircuitBreaker功能

欢送拜访我的 GitHub

https://github.com/zq2599/blog_demos

内容:所有原创文章分类汇总及配套源码,波及 Java、Docker、Kubernetes、DevOPS 等;

本篇概览

  • 一起深刻理解 Spring Cloud Gateway 的断路器 (CircuitBreaker) 性能:
  • 先聊聊实践
  • 再联合官网和大神的信息确定技术栈
  • 再入手开发,先实现再验证
  • 再趁热打铁,看看它的源码
  • 最初,回顾一下有哪些有余(下一篇文章解决这些有余)

对于断路器(CircuitBreaker)

  • 下图来自 resilience4j 官网文档,介绍了什么是断路器:

  1. CLOSED 状态时,申请失常放行
  2. 申请失败率达到设定阈值时,变为 OPEN 状态,此时申请全副不放行
  3. OPEN 状态继续设定工夫后,进入半开状态(HALE_OPEN),放过局部申请
  4. 半开状态下,失败率低于设定阈值,就进入 CLOSE 状态,即全副放行
  5. 半开状态下,失败率高于设定阈值,就进入 OPEN 状态,即全副不放行

确认概念

  • 有个概念先确认一下,即 <font color=”blue”>Spring Cloud 断路器 </font> 与 <font color=”blue”>Spring Cloud Gateway 断路器性能 </font> 不是同一个概念,Spring Cloud Gateway 断路器性能还波及过滤器,即在过滤器的规定下应用断路器:

  • 本篇的重点是 Spring Cloud Gateway 如何配置和应用断路器(CircuitBreaker),因而不会探讨 Resilience4J 的细节,如果您想深刻理解 Resilience4J,举荐材料是 Spring Cloud Circuit Breaker

对于 Spring Cloud 断路器

  • 先看 Spring Cloud 断路器,如下图,Hystrix、Sentinel 这些都是相熟的概念:

对于 Spring Cloud Gateway 的断路器性能

  • 来看 Spring Cloud Gateway 的官网文档,如下图,有几个关键点稍后介绍:

  • 上图走漏了几个要害信息:
  • Spring Cloud Gateway 内置了断路器 filter,
  • 具体做法是应用 Spring Cloud 断路器的 API,将 gateway 的路由逻辑封装到断路器中
  • 有多个断路器的库都能够用在 Spring Cloud Gateway(遗憾的是没有列举是哪些)
  • Resilience4J 对 Spring Cloud 来说是开箱即用的
  • 简略来说 Spring Cloud Gateway 的断路器性能是通过内置 filter 实现的,这个 filter 应用了 Spring Cloud 断路器;
  • 官网说多个断路器的库都能够用在 Spring Cloud Gateway,然而并没有说具体是哪些,这就郁闷了,此时咱们去理解一位牛人的观点:Piotr Mińkowski,就是上面这本书的作者:

  • Piotr Mińkowski 的博客对 Spring Cloud Gateway 的断路器性能做了具体介绍,如下图,有几个重要信息稍后会提到:

  • 上图能够 get 到三个要害信息:
  • 从 2.2.1 版本起,Spring Cloud Gateway 集成了 Resilience4J 的断路器实现
  • Netflix 的 Hystrix 进入了维护阶段(能了解为行将退休吗?)
  • Netflix 的 Hystrix 仍然可用,然而已废除(deprecated),而且 Spring Cloud 未来的版本可能会不反对
  • 再关联到官网文档也以 resilience4 为例(如下图),胆大的我仿佛没有别的抉择了,就 Resilience4J 吧:

  • 实践剖析就到此吧,接下来开始实战,具体的步骤如下:
  • 筹备工作:服务提供者新增一个 web 接口 <font color=”blue”>/account/{id}</font>,依据入参的不同,该接口能够立刻返回或者延时 500 毫秒返回
  • 新增名为 <font color=”blue”>circuitbreaker-gateway</font> 的子工程,这是个带有断路器性能的 Spring Cloud Gateway 利用
  • 在 <font color=”blue”>circuitbreaker-gateway</font> 外面编写单元测试代码,用来验证断路器是否失常
  • 运行单元测试代码,察看断路器是否失效
  • 给断路器增加 fallback 并验证是否失效
  • 做一次简略的源码剖析,一为想深刻理解断路器的同学捋分明源码门路,二为测验本人以前理解的 springboot 常识在浏览源码时有么有帮忙

源码下载

  • 本篇实战中的残缺源码可在 GitHub 下载到,地址和链接信息如下表所示(https://github.com/zq2599/blo…):
名称 链接 备注
我的项目主页 https://github.com/zq2599/blo… 该我的项目在 GitHub 上的主页
git 仓库地址(https) https://github.com/zq2599/blo… 该我的项目源码的仓库地址,https 协定
git 仓库地址(ssh) git@github.com:zq2599/blog_demos.git 该我的项目源码的仓库地址,ssh 协定
  • 这个 git 我的项目中有多个文件夹,本篇的源码在 <font color=”blue”>spring-cloud-tutorials</font> 文件夹下,如下图红框所示:

  • <font color=”blue”>spring-cloud-tutorials</font> 文件夹下有多个子工程,本篇的代码是 <font color=”red”>circuitbreaker-gateway</font>,如下图红框所示:

筹备工作

  • 咱们要筹备一个可控的 web 接口,通过参数管制它胜利或者失败,这样能力触发断路器
  • 本篇的实战中,服务提供者仍旧是 <font color=”blue”>provider-hello</font>,为了满足本次实战的需要,咱们在 Hello.java 文件中减少一个 web 接口,对应的源码如下:
    @RequestMapping(value = "/account/{id}", method = RequestMethod.GET)
    public String account(@PathVariable("id") int id) throws InterruptedException {if(1==id) {Thread.sleep(500);
        }

        return Constants.ACCOUNT_PREFIX + dateStr();}
  • 上述代码很简略:就是接管 id 参数,如果等于 1 就延时五百毫秒,不等于 1 就立刻返回
  • 如果把断路器设置为超过两百毫秒就算失败,那么通过管制 id 参数的值,咱们就能模仿申请胜利或者失败了,<font color=”blue”> 这是验证断路器性能的要害 </font>
  • 筹备实现,开始写代码

实战

  • 在父工程 <font color=”blue”>spring-cloud-tutorials</font> 上面新增子工程 <font color=”blue”>circuitbreaker-gateway</font>
  • 减少以下依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
  • 配置文件 application.yml 如下:
server:
  #服务端口
  port: 8081
spring:
  application:
    name: circuitbreaker-gateway
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://127.0.0.1:8082
          predicates:
            - Path=/hello/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
  • 启动类:
package com.bolingcavalry.circuitbreakergateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CircuitbreakerApplication {public static void main(String[] args) {SpringApplication.run(CircuitbreakerApplication.class,args);
    }
}
  • 配置类如下,这是断路器相干的参数配置:
package com.bolingcavalry.circuitbreakergateway.config;

import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;

@Configuration
public class CustomizeCircuitBreakerConfig {

    @Bean
    public ReactiveResilience4JCircuitBreakerFactory defaultCustomizer() {CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() //
                .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED) // 滑动窗口的类型为工夫窗口
                .slidingWindowSize(10) // 工夫窗口的大小为 60 秒
                .minimumNumberOfCalls(5) // 在单位工夫窗口内起码须要 5 次调用能力开始进行统计计算
                .failureRateThreshold(50) // 在单位工夫窗口内调用失败率达到 50% 后会启动断路器
                .enableAutomaticTransitionFromOpenToHalfOpen() // 容许断路器主动由关上状态转换为半开状态
                .permittedNumberOfCallsInHalfOpenState(5) // 在半开状态下容许进行失常调用的次数
                .waitDurationInOpenState(Duration.ofSeconds(5)) // 断路器关上状态转换为半开状态须要期待 60 秒
                .recordExceptions(Throwable.class) // 所有异样都当作失败来解决
                .build();

        ReactiveResilience4JCircuitBreakerFactory factory = new ReactiveResilience4JCircuitBreakerFactory();
        factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
                .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(200)).build())
                .circuitBreakerConfig(circuitBreakerConfig).build());

        return factory;
    }
}
  • 上述代码有一次须要留神:<font color=”blue”>timeLimiterConfig</font> 办法设置了超时工夫,服务提供者如果超过 200 毫秒没有响应,Spring Cloud Gateway 就会向调用者返回失败
  • 开发实现了,接下来要思考的是如何验证

单元测试类

  • 为了验证 Spring Cloud Gateway 的断路器性能,咱们能够用 Junit 单元测试来准确管制申请参数和申请次数,测试类如下,可见测试类会间断发一百次申请,在前五十次中,申请参数始终在 0 和 1 之间切换,参数等于 1 的时候,接口会有 500 毫秒延时,超过了 Spring Cloud Gateway 的 200 毫秒超时限度,这时候就会返回失败,等失败多了,就会触发断路器的断开:
package com.bolingcavalry.circuitbreakergateway;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@ExtendWith(SpringExtension.class)
@AutoConfigureWebTestClient
public class CircuitbreakerTest {

    // 测试的总次数
    private static int i=0;

    @Autowired
    private WebTestClient webClient;

    @Test
    @RepeatedTest(100)
    void testHelloPredicates() throws InterruptedException {
        // 低于 50 次时,gen 在 0 和 1 之间切换,也就是一次失常一次超时,// 超过 50 次时,gen 固定为 0,此时每个申请都不会超时
        int gen = (i<50) ? (i % 2) : 0;

        // 次数加一
        i++;

        final String tag = "[" + i + "]";

        // 发动 web 申请
        webClient.get()
                .uri("/hello/account/" + gen)
                .accept(MediaType.APPLICATION_JSON)
                .exchange()
                .expectBody(String.class).consumeWith(result  -> System.out.println(tag + result.getRawStatusCode() + "-" + result.getResponseBody()));

        Thread.sleep(1000);
    }
}

验证

  • 启动 nacos(服务提供者依赖的)
  • 启动子工程 <font color=”blue”>provider-hello</font>
  • 运行咱们方才开发的单元测试类,控制台输出的内容截取局部如下,稍后会有剖析:
[2]504 - {"timestamp":"2021-08-28T02:55:42.920+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"594efed1"}
[3]200 - Account2021-08-28 10:55:43
[4]504 - {"timestamp":"2021-08-28T02:55:45.177+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"427720b"}
[5]200 - Account2021-08-28 10:55:46
[6]503 - {"timestamp":"2021-08-28T02:55:47.227+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"6595d7f4"}
[7]503 - {"timestamp":"2021-08-28T02:55:48.250+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"169ae1c"}
[8]503 - {"timestamp":"2021-08-28T02:55:49.259+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"53b695a1"}
[9]503 - {"timestamp":"2021-08-28T02:55:50.269+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"4a072f52"}
[10]504 - {"timestamp":"2021-08-28T02:55:51.499+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4bdd96c4"}
[11]200 - Account2021-08-28 10:55:52
[12]504 - {"timestamp":"2021-08-28T02:55:53.745+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4e0e7eab"}
[13]200 - Account2021-08-28 10:55:54
[14]504 - {"timestamp":"2021-08-28T02:55:56.013+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"27685405"}
[15]503 - {"timestamp":"2021-08-28T02:55:57.035+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"3e40c5db"}
[16]503 - {"timestamp":"2021-08-28T02:55:58.053+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"2bf2698b"}
[17]503 - {"timestamp":"2021-08-28T02:55:59.075+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"38cb1840"}
[18]503 - {"timestamp":"2021-08-28T02:56:00.091+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"21586fa"}
[19]200 - Account2021-08-28 10:56:01
[20]504 - {"timestamp":"2021-08-28T02:56:02.325+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4014d6d4"}
[21]200 - Account2021-08-28 10:56:03
[22]504 - {"timestamp":"2021-08-28T02:56:04.557+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"173a3b9d"}
[23]200 - Account2021-08-28 10:56:05
[24]504 - {"timestamp":"2021-08-28T02:56:06.811+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"aa8761f"}
[25]200 - Account2021-08-28 10:56:07
[26]504 - {"timestamp":"2021-08-28T02:56:09.057+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"769bfefc"}
[27]200 - Account2021-08-28 10:56:10
[28]504 - {"timestamp":"2021-08-28T02:56:11.314+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"2fbcb6c0"}
[29]503 - {"timestamp":"2021-08-28T02:56:12.332+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"58e4e70f"}
[30]503 - {"timestamp":"2021-08-28T02:56:13.342+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"367651c5"}
  • 剖析上述输入的返回码:
  1. 504 是超时返回的谬误,200 是服务提供者的失常返回
  2. 504 和 200 两种返回码都示意申请达到了服务提供者,所以此时断路器是敞开状态
  3. 屡次 504 谬误后,达到了配置的门限,触发断路器开启
  4. 间断呈现的 503 就是断路器开启后的返回码,此时申请是无奈达到服务提供者的
  5. 间断的 503 之后,504 和 200 再次交替呈现,证实此时进入半开状态,而后 504 再次达到门限触发断路器从半开转为开启,五十次之后,因为不在发送超时申请,断路器进入敞开状态

fallback

  • 通过上述测试可见,Spring Cloud Gateway 通过返回码来告知调用者错误信息,这种形式不够敌对,咱们能够自定义 fallback,在返回谬误时由它来构建返回信息
  • 再开发一个 web 接口,没错,就是在 <font color=”blue”>circuitbreaker-gateway</font> 工程中增加一个 web 接口:
package com.bolingcavalry.circuitbreakergateway.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;

@RestController
public class Fallback {private String dateStr(){return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
    }

    /**
     * 返回字符串类型
     * @return
     */
    @GetMapping("/myfallback")
    public String helloStr() {return "myfallback," + dateStr();
    }
}
  • application.yml 配置如下,可见是给 filter 减少了 <font color=”blue”>fallbackUri</font> 属性:
server:
  #服务端口
  port: 8081
spring:
  application:
    name: circuitbreaker-gateway
  cloud:
    gateway:
      routes:
        - id: path_route
          uri: http://127.0.0.1:8082
          predicates:
            - Path=/hello/**
          filters:
            - name: CircuitBreaker
              args:
                name: myCircuitBreaker
                fallbackUri: forward:/myfallback
  • 再运行单元测试,可见返回码全副是 200,原来的谬误当初全副变成了方才新增的接口的返回内容:
[2]200 - myfallback, 2021-08-28 11:15:02
[3]200 - Account2021-08-28 11:15:03
[4]200 - myfallback, 2021-08-28 11:15:04
[5]200 - Account2021-08-28 11:15:05
[6]200 - myfallback, 2021-08-28 11:15:06
[7]200 - myfallback, 2021-08-28 11:15:08
[8]200 - myfallback, 2021-08-28 11:15:09
[9]200 - myfallback, 2021-08-28 11:15:10
[10]200 - myfallback, 2021-08-28 11:15:11
[11]200 - Account2021-08-28 11:15:12
[12]200 - myfallback, 2021-08-28 11:15:13
[13]200 - Account2021-08-28 11:15:14
[14]200 - myfallback, 2021-08-28 11:15:15
  • 至此,咱们已实现了 Spring Cloud Gateway 的断路器性能的开发和测试,如果聪慧好学的您并不满足这寥寥几行配置和代码,想要深刻理解断路器的外部,那么请您接往下看,咱们聊聊它的源码;

源码剖析

  • RouteDefinitionRouteLocator 的构造方法(bean 注入)中有如下代码,将 name 和实例绑定:
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
  • 而后会在 loadGatewayFilters 办法中应用这个 map,找到下面 put 的 bean;
  • 最终的成果:路由配置中指定了 name 等于 <font color=”blue”>CircuitBreaker</font>,即可对应 SpringCloudCircuitBreakerFilterFactory 类型的 bean,因为它的 name 办法返回了 ”CircuitBreaker”,如下图:

  • 当初的问题:SpringCloudCircuitBreakerFilterFactory 类型的 bean 是什么?如下图红框,SpringCloudCircuitBreakerResilience4JFilterFactory 是 SpringCloudCircuitBreakerFilterFactory 惟一的子类:

  • 从上图来看,CircuitBreaker 类型的 filter 应该是 SpringCloudCircuitBreakerResilience4JFilterFactory,不过那只是从继承关系推断进去的,还差一个要害证据:在 spring 中,到底存不存在 SpringCloudCircuitBreakerResilience4JFilterFactory 类型的 bean?
  • 最终发现了 GatewayResilience4JCircuitBreakerAutoConfiguration 中的配置,能够证实 SpringCloudCircuitBreakerResilience4JFilterFactory 会被实例化并注册到 spring:
@Bean
    @ConditionalOnBean(ReactiveResilience4JCircuitBreakerFactory.class)
    @ConditionalOnEnabledFilter
    public SpringCloudCircuitBreakerResilience4JFilterFactory springCloudCircuitBreakerResilience4JFilterFactory(
            ReactiveResilience4JCircuitBreakerFactory reactiveCircuitBreakerFactory,
            ObjectProvider<DispatcherHandler> dispatcherHandler) {return new SpringCloudCircuitBreakerResilience4JFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler);
    }
  • 综上所述,当您配置了 CircuitBreaker 过滤器时,实际上是 SpringCloudCircuitBreakerResilience4JFilterFactory 类在为您服务,而要害代码都集中在其父类 SpringCloudCircuitBreakerFilterFactory 中;
  • 所以,要想深刻理解 Spring Cloud Gateway 的断路器性能,请浏览 SpringCloudCircuitBreakerFilterFactory.apply 办法

一点遗憾

  • 还记得方才剖析控制台输入的那段内容吗?就是下图红框中的那段,过后咱们用返回码来揣测断路器处于什么状态:

  • 置信您在看这段纯文字时,对欣宸的剖析还是存在纳闷的,依据返回码就把断路器的状态确定了?例如 504 的时候到底是敞开还是半开呢?都有可能吧,所以,这种揣测只能证实断路器正在工作,然而无奈确定某个时刻具体的状态
  • 所以,咱们须要一种更精确的形式晓得每个时刻断路器的状态,这样才算对断路器有了粗浅理解
  • 接下来的文章中,咱们在明天的成绩上更进一步,在申请中把断路器状态打印进去,那就 … 敬请期待吧,欣宸原创,从未让您悲观;

你不孤独,欣宸原创一路相伴

  1. Java 系列
  2. Spring 系列
  3. Docker 系列
  4. kubernetes 系列
  5. 数据库 + 中间件系列
  6. DevOps 系列

欢送关注公众号:程序员欣宸

微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游 Java 世界 …
https://github.com/zq2599/blog_demos

退出移动版