欢送拜访我的GitHub
https://github.com/zq2599/blog_demos
内容:所有原创文章分类汇总及配套源码,波及Java、Docker、Kubernetes、DevOPS等;
本篇概览
- 一起深刻理解Spring Cloud Gateway的断路器(CircuitBreaker)性能:
- 先聊聊实践
- 再联合官网和大神的信息确定技术栈
- 再入手开发,先实现再验证
- 再趁热打铁,看看它的源码
- 最初,回顾一下有哪些有余(下一篇文章解决这些有余)
对于断路器(CircuitBreaker)
- 下图来自resilience4j官网文档,介绍了什么是断路器:
- CLOSED状态时,申请失常放行
- 申请失败率达到设定阈值时,变为OPEN状态,此时申请全副不放行
- OPEN状态继续设定工夫后,进入半开状态(HALE_OPEN),放过局部申请
- 半开状态下,失败率低于设定阈值,就进入CLOSE状态,即全副放行
- 半开状态下,失败率高于设定阈值,就进入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: 8081spring: 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;@SpringBootApplicationpublic 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;@Configurationpublic 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)@AutoConfigureWebTestClientpublic 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"}
- 剖析上述输入的返回码:
- 504是超时返回的谬误,200是服务提供者的失常返回
- 504和200两种返回码都示意申请达到了服务提供者,所以此时断路器是敞开状态
- 屡次504谬误后,达到了配置的门限,触发断路器开启
- 间断呈现的503就是断路器开启后的返回码,此时申请是无奈达到服务提供者的
- 间断的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;@RestControllerpublic 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: 8081spring: 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的时候到底是敞开还是半开呢?都有可能吧,所以,这种揣测只能证实断路器正在工作,然而无奈确定某个时刻具体的状态
- 所以,咱们须要一种更精确的形式晓得每个时刻断路器的状态,这样才算对断路器有了粗浅理解
- 接下来的文章中,咱们在明天的成绩上更进一步,在申请中把断路器状态打印进去,那就...敬请期待吧,欣宸原创,从未让您悲观;
你不孤独,欣宸原创一路相伴
- Java系列
- Spring系列
- Docker系列
- kubernetes系列
- 数据库+中间件系列
- DevOps系列
欢送关注公众号:程序员欣宸
微信搜寻「程序员欣宸」,我是欣宸,期待与您一起畅游Java世界...
https://github.com/zq2599/blog_demos