SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)
在前面一节,我们利用 resilience4j 粘合了 OpenFeign 实现了断路器、重试以及线程隔离,并使用了新的负载均衡算法优化了业务激增时的负载均衡算法表现。这一节,我们开始编写单元测试验证这些功能的正确性,以便于日后升级依赖,修改的时候能保证正确性。同时,通过单元测试,我们更能深入理解 Spring Cloud。
验证重试配置
对于我们实现的重试,我们需要验证:
- 验证配置正确加载:即我们在 Spring 配置(例如
application.yml
)中的加入的 Resilience4j 的配置被正确加载应用了。 - 验证针对 ConnectTimeout 重试正确:FeignClient 可以配置 ConnectTimeout 连接超时时间,如果连接超时会有连接超时异常抛出,对于这种异常无论什么请求都应该重试,因为请求并没有发出。
- 验证针对断路器异常的重试正确:断路器是微服务实例方法级别的,如果抛出断路器打开异常,应该直接重试下一个实例。
- 验证针对限流器异常的重试正确:当某个实例线程隔离满了的时候,抛出线程限流异常应该直接重试下一个实例。
- 验证针对非 2xx 响应码可重试的方法重试正确
- 验证针对非 2xx 响应码不可重试的方法没有重试
- 验证针对可重试的方法响应超时异常重试正确:FeignClient 可以配置 ReadTimeout 即响应超时,如果方法可以重试,则需要重试。
- 验证针对不可重试的方法响应超时异常不能重试:FeignClient 可以配置 ReadTimeout 即响应超时,如果方法不可以重试,则不能重试。
验证配置正确加载
我们可以定义不同的 FeignClient,之后检查 resilience4j 加载的重试配置来验证重试配置的正确加载。
首先定义两个 FeignClient,微服务分别是 testService1 和 testService2,contextId 分别是 testService1Client 和 testService2Client
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
@FeignClient(name = "testService2", contextId = "testService2Client")
public interface TestService2Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
然后,我们增加 Spring 配置,使用 SpringExtension 编写单元测试类:
//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
// testService2Client 里面的所有方法请求重试次数为 2
"resilience4j.retry.configs.testService2Client.maxAttempts=2",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
}
}
编写测试代码,验证配置加载正确性:
@Test
public void testConfigureRetry() {
//读取所有的 Retry
List<Retry> retries = retryRegistry.getAllRetries().asJava();
//验证其中的配置是否符合我们填写的配置
Map<String, Retry> retryMap = retries.stream().collect(Collectors.toMap(Retry::getName, v -> v));
//我们初始化 Retry 的时候,使用 FeignClient 的 ContextId 作为了 Retry 的 Name
Retry retry = retryMap.get("testService1Client");
//验证 Retry 配置存在
Assertions.assertNotNull(retry);
//验证 Retry 配置符合我们的配置
Assertions.assertEquals(retry.getRetryConfig().getMaxAttempts(), 3);
retry = retryMap.get("testService2Client");
//验证 Retry 配置存在
Assertions.assertNotNull(retry);
//验证 Retry 配置符合我们的配置
Assertions.assertEquals(retry.getRetryConfig().getMaxAttempts(), 2);
}
验证针对 ConnectTimeout 重试正确
我们可以通过针对一个微服务注册两个实例,一个实例是连接不上的,另一个实例是可以正常连接的,无论怎么调用 FeignClient,请求都不会失败,来验证重试是否生效。我们使用 HTTP 测试网站来测试,即 http://httpbin.org 。这个网站的 api 可以用来模拟各种调用。其中 /status/{status}
就是将发送的请求原封不动的在响应中返回。在单元测试中,我们不会单独部署一个注册中心,而是直接 Mock spring cloud 中服务发现的核心接口 DiscoveryClient,并且将我们 Eureka 的服务发现以及注册通过配置都关闭,即:
//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//关闭 eureka client
"eureka.client.enabled=false",
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3"
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service1Instance4 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service1Instance4.getInstanceId()).thenReturn("service1Instance4");
when(service1Instance4.getHost()).thenReturn("www.httpbin.org");
//这个port连不上,测试 IOException
when(service1Instance4.getPort()).thenReturn(18080);
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
//微服务 testService3 有两个实例即 service1Instance1 和 service1Instance4
Mockito.when(spy.getInstances("testService3"))
.thenReturn(List.of(service1Instance1, service1Instance4));
return spy;
}
}
}
编写 FeignClient:
@FeignClient(name = "testService3", contextId = "testService3Client")
public interface TestService3Client {
@PostMapping("/anything")
HttpBinAnythingResponse anything();
}
调用 TestService3Client 的 anything
方法,验证是否有重试:
@SpyBean
private TestService3Client testService3Client;
/**
* 验证对于有不正常实例(正在关闭的实例,会 connect timeout)请求是否正常重试
*/
@Test
public void testIOExceptionRetry() {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
for (int i = 0; i < 5; i++) {
Span span = tracer.nextSpan();
try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
//不抛出异常,则正常重试了
testService3Client.anything();
testService3Client.anything();
}
}
}
这里强调一点,由于我们在这个类中还会测试其他异常,以及断路器,我们需要避免这些测试一起执行的时候,断路器打开了,所以我们在所有测试调用 FeignClient 的方法开头,清空所有断路器的数据,通过:
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
并且通过日志中可以看出由于 connect timeout 进行重试:
call url: POST -> http://www.httpbin.org:18080/anything, ThreadPoolStats(testService3Client:www.httpbin.org:18080): {"coreThreadPoolSize":10,"maximumThreadPoolSize":10,"queueCapacity":100,"queueDepth":0,"remainingQueueCapacity":100,"threadPoolSize":1}, CircuitBreakStats(testService3Client:www.httpbin.org:18080:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService3Client.anything()): {"failureRate":-1.0,"numberOfBufferedCalls":0,"numberOfFailedCalls":0,"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfSlowFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"slowCallRate":-1.0}
TestService3Client#anything() response: 582-Connect to www.httpbin.org:18080 [www.httpbin.org/34.192.79.103, www.httpbin.org/18.232.227.86, www.httpbin.org/3.216.167.140, www.httpbin.org/54.156.165.4] failed: Connect timed out, should retry: true
call url: POST -> http://httpbin.org:80/anything, ThreadPoolStats(testService3Client:httpbin.org:80): {"coreThreadPoolSize":10,"maximumThreadPoolSize":10,"queueCapacity":100,"queueDepth":0,"remainingQueueCapacity":100,"threadPoolSize":1}, CircuitBreakStats(testService3Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService3Client.anything()): {"failureRate":-1.0,"numberOfBufferedCalls":0,"numberOfFailedCalls":0,"numberOfNotPermittedCalls":0,"numberOfSlowCalls":0,"numberOfSlowFailedCalls":0,"numberOfSlowSuccessfulCalls":0,"numberOfSuccessfulCalls":0,"slowCallRate":-1.0}
response: 200 - OK
验证针对断路器异常的重试正确
通过系列前面的源码分析,我们知道 spring-cloud-openfeign 的 FeignClient 其实是懒加载的。所以我们实现的断路器也是懒加载的,需要先调用,之后才会初始化断路器。所以这里如果我们要模拟断路器打开的异常,需要先手动读取载入断路器,之后才能获取对应方法的断路器,修改状态。
我们先定义一个 FeignClient:
@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
@GetMapping("/anything")
HttpBinAnythingResponse anything();
}
使用前面同样的方式,给这个微服务添加实例:
//SpringExtension也包含了 Mockito 相关的 Extension,所以 @Mock 等注解也生效了
@ExtendWith(SpringExtension.class)
@SpringBootTest(properties = {
//关闭 eureka client
"eureka.client.enabled=false",
//默认请求重试次数为 3
"resilience4j.retry.configs.default.maxAttempts=3",
//增加断路器配置
"resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
"resilience4j.circuitbreaker.configs.default.slidingWindowType=COUNT_BASED",
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
})
@Log4j2
public class OpenFeignClientTest {
@SpringBootApplication
@Configuration
public static class App {
@Bean
public DiscoveryClient discoveryClient() {
//模拟两个服务实例
ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
ServiceInstance service1Instance3 = Mockito.spy(ServiceInstance.class);
Map<String, String> zone1 = Map.ofEntries(
Map.entry("zone", "zone1")
);
when(service1Instance1.getMetadata()).thenReturn(zone1);
when(service1Instance1.getInstanceId()).thenReturn("service1Instance1");
when(service1Instance1.getHost()).thenReturn("httpbin.org");
when(service1Instance1.getPort()).thenReturn(80);
when(service1Instance3.getMetadata()).thenReturn(zone1);
when(service1Instance3.getInstanceId()).thenReturn("service1Instance3");
//这其实就是 httpbin.org ,为了和第一个实例进行区分加上 www
when(service1Instance3.getHost()).thenReturn("www.httpbin.org");
DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
//微服务 testService3 有两个实例即 service1Instance1 和 service1Instance4
Mockito.when(spy.getInstances("testService1"))
.thenReturn(List.of(service1Instance1, service1Instance3));
return spy;
}
}
}
然后,编写测试代码:
@Test
public void testRetryOnCircuitBreakerException() {
//防止断路器影响
circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
CircuitBreaker testService1ClientInstance1Anything;
try {
testService1ClientInstance1Anything = circuitBreakerRegistry
.circuitBreaker("testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()", "testService1Client");
} catch (ConfigurationNotFoundException e) {
//找不到就用默认配置
testService1ClientInstance1Anything = circuitBreakerRegistry
.circuitBreaker("testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()");
}
//将断路器打开
testService1ClientInstance1Anything.transitionToOpenState();
//调用多次,调用成功即对断路器异常重试了
for (int i = 0; i < 10; i++) {
this.testService1Client.anything();
}
}
运行测试,日志中可以看出,针对断路器打开的异常进行重试了:
2021-11-13 03:40:13.546 INFO [,,] 4388 --- [ main] c.g.j.s.c.w.f.DefaultErrorDecoder : TestService1Client#anything() response: 581-CircuitBreaker 'testService1Client:httpbin.org:80:public abstract com.github.jojotech.spring.cloud.webmvc.test.feign.HttpBinAnythingResponse com.github.jojotech.spring.cloud.webmvc.test.feign.OpenFeignClientTest$TestService1Client.anything()' is OPEN and does not permit further calls, should retry: true
微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:
SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)的更多相关文章
- SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(2)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节针对我们的重试进行测试 验证针对限流器异常的重试正确 通过系列前面的源码分析 ...
- SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(3)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节针对我们的重试进行测试 验证针对可重试的方法响应超时异常重试正确 我们可以通 ...
- SpringCloud升级之路2020.0.x版-35. 验证线程隔离正确性
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 上一节我们通过单元测试验证了重试的正确性,这一节我们来验证我们线程隔离的正确性,主要包括: ...
- SpringCloud升级之路2020.0.x版-33. 实现重试、断路器以及线程隔离源码
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面两节,我们梳理了实现 Feign 断路器以及线程隔离的思路,并说明了如何优化目前的负 ...
- SpringCloud升级之路2020.0.x版-36. 验证断路器正确性
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 上一节我们通过单元测试验证了线程隔离的正确性,这一节我们来验证我们断路器的正确性,主要包括 ...
- SpringCloud升级之路2020.0.x版-13.UnderTow 核心配置
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford Undertow ...
- SpringCloud升级之路2020.0.x版-14.UnderTow AccessLog 配置介绍
本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford server: u ...
- SpringCloud升级之路2020.0.x版-1.背景
本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...
- SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)
本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...
随机推荐
- P5056-[模板]插头dp
正题 题目链接:https://www.luogu.com.cn/problem/P5056 题目大意 \(n*m\)的网格,求有多少条回路可以铺满整个棋盘. 解题思路 插头\(dp\)的,写法是按照 ...
- ASP.NET Core 中间件的使用(三):全局异常处理机制
前言 我们经常听到"秒修复秒上线",觉得很厉害的样子. 其实不然,这只是一个调侃而已,出现问题的方式很多(逻辑漏洞.代码异常.操作方式不正确等). 我们今天来说代码异常问题怎么快速 ...
- Redis缓存穿透、缓存击穿、缓存雪崩的介绍及其解决方案
首先,来画一张图了解下缓存处理的流程 一.缓存穿透 描述: 缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求查询该数据,导致数据库压力过大. 解决方案: 1.接口校验 如鉴权校验.数据合法性 ...
- 分布式应用开发 | SpringBoot+dubbo+zookeeper实现服务注册发现 | 远程服务调用
前言 通过新建两个独立服务--提供者.消费者,模拟两个独立分布的应用,通过使用dubbo+zookeeper来实现远程服务调用. 目录 项目搭建 provider-server consumer-se ...
- java的加载与执行原理剖析
到目前为止,我们接触过的重点术语,总结一下: Java体系的技术被划分为三大块: JavaSE:标准版 JavaEE:企业版 JavaME:微型版 安装JDK之后: JDK:java开发工具箱 JRE ...
- 【c++ Prime 学习笔记】第5章 语句
C++提供了一组控制流语句,包括条件执行语句.循环语句.跳转语句. 5.1 简单语句 空语句 ; ,最简单的语句 别漏写分号,也别多写 while(cin>>s && s! ...
- webpack基础以及webpack中babel的配置
webpack 安装 npm 初始化,控制台输入 npm init -y webpack 安装 npm i webpack webpack-cli -D 新建 webpack.config.js co ...
- 热身训练4 Eighty seven
Eighty seven 简要题意: n个卡片,其中第i个卡片的数值为$a[i]$.一共q次询问,每次询问将删去其中3个卡片(可能删除若干相同的卡片)后,问能否选出10个卡片,数值之和等于87. n≤ ...
- gdal3.1.0+VS2017+geos+kml编译总结
1.简介 gdal3.1.0编译过程中必须依赖proj,编译gdal必须要编译proj,proj的编译需要sqlite3,因此想要编译gdal3.1.0需要先编译proj和sqlite3 2.关于sq ...
- ab矩阵(实对称矩阵)
今天在做题时巧遇了很多此类型的矩阵,出于更快解,对此进行学习.(感谢up主线帒杨) 1.认识ab矩阵 形如:主对角线元素都是a,其余元素都是b,我们称之为ab矩阵(默认涉及即为n×n阶) 2.求|A| ...