共计 9704 个字符,预计需要花费 25 分钟才能阅读完成。
本系列代码地址:https://github.com/JoJoTec/sp…
在后面一节,咱们利用 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: