专栏目录
26.OpenFeign的组件 4.maven依赖回顾以及项目框架结构 39. 改造 resilience4j 粘合 WebClient 3.Eureka Server 与 API 网关要考虑的问题 5.所有项目的parent与spring-framework-common说明 13.UnderTow 核心配置 29.Spring Cloud OpenFeign 的解析(1) 45. 实现公共日志记录 44.避免链路信息丢失做的设计(2) 35. 验证线程隔离正确性 31. FeignClient 实现断路器以及线程隔离限流的思路 37. 实现异步的客户端封装配置管理的意义与设计 23.订制Spring Cloud LoadBalancer 21.Spring Cloud LoadBalancer简介 10.使用Log4j2以及一些核心配置 17.Eureka的实例配置 11.Log4j2 监控相关 1. 背景 7.从Bean到SpringCloud 38. 实现自定义 WebClient 的 NamedContextFactory 15.UnderTow 订制 18.Eureka的客户端核心设计和配置 41. SpringCloudGateway 基本流程讲解(1) 6.微服务特性相关的依赖说明 42.SpringCloudGateway 现有的可供分析的请求日志以及缺陷 43.为何 SpringCloudGateway 中会有链路信息丢失 34.验证重试配置正确性 28.OpenFeign的生命周期-进行调用 20. 启动一个 Eureka Server 集群 14.UnderTow AccessLog 配置介绍 41. SpringCloudGateway 基本流程讲解(2) 44.避免链路信息丢失做的设计(1) 16.Eureka架构和核心概念 22.Spring Cloud LoadBalancer核心源码 9.如何理解并定制一个Spring Cloud组件 27.OpenFeign的生命周期-创建代理 32. 改进负载均衡算法 24.测试Spring Cloud LoadBalancer 19.Eureka的服务端设计与配置 8.理解 NamedContextFactory 12.UnderTow 简介与内部原理 40. spock 单元测试封装的 WebClient(下) 33. 实现重试、断路器以及线程隔离源码 36. 验证断路器正确性 40. spock 单元测试封装的 WebClient(上) 30. FeignClient 实现重试 2.微服务框架需要考虑的问题 25.OpenFeign简介与使用

36. 验证断路器正确性

unknown
• 阅读 810

36. 验证断路器正确性

本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent

上一节我们通过单元测试验证了线程隔离的正确性,这一节我们来验证我们断路器的正确性,主要包括:

  1. 验证配置正确加载:即我们在 Spring 配置(例如 application.yml)中的加入的 Resilience4j 的配置被正确加载应用了。
  2. 验证断路器是基于服务和方法打开的,也就是某个微服务的某个方法断路器打开但是不会影响这个微服务的其他方法调用

验证配置正确加载

与之前验证重试类似,我们可以定义不同的 FeignClient,之后检查 resilience4j 加载的断路器配置来验证线程隔离配置的正确加载。

并且,与重试配置不同的是,通过系列前面的源码分析,我们知道 spring-cloud-openfeign 的 FeignClient 其实是懒加载的。所以我们实现的断路器也是懒加载的,需要先调用,之后才会初始化断路器。所以这里我们需要先进行调用之后,再验证断路器配置。

首先定义两个 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",
        //默认断路器配置
        "resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
        "resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=2",
        //testService2Client 的 断路器配置
        "resilience4j.circuitbreaker.configs.testService2Client.failureRateThreshold=30",
        "resilience4j.circuitbreaker.configs.testService2Client.minimumNumberOfCalls=10",

})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    public static class App {
        @Bean
        public DiscoveryClient discoveryClient() {
            //模拟两个服务实例
            ServiceInstance service1Instance1 = Mockito.spy(ServiceInstance.class);
            ServiceInstance service2Instance2 = 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("www.httpbin.org");
            when(service1Instance1.getPort()).thenReturn(80);
            when(service2Instance2.getInstanceId()).thenReturn("service1Instance2");
            when(service2Instance2.getHost()).thenReturn("httpbin.org");
            when(service2Instance2.getPort()).thenReturn(80);
            DiscoveryClient spy = Mockito.spy(DiscoveryClient.class);
            Mockito.when(spy.getInstances("testService1"))
                    .thenReturn(List.of(service1Instance1));
            Mockito.when(spy.getInstances("testService2"))
                    .thenReturn(List.of(service2Instance2));
            return spy;
        }
    }
}

编写测试代码,验证配置正确:

@Test
    public void testConfigureCircuitBreaker() {
        //防止断路器影响
        circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
        //调用下这两个 FeignClient 确保对应的 NamedContext 被初始化
        testService1Client.anything();
        testService2Client.anything();
        //验证断路器的实际配置,符合我们的填入的配置
        List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
        Set<String> collect = circuitBreakers.stream().map(CircuitBreaker::getName)
                .filter(name -> {
                    try {
                        return name.contains(TestService1Client.class.getMethod("anything").toGenericString())
                                || name.contains(TestService2Client.class.getMethod("anything").toGenericString());
                    } catch (NoSuchMethodException e) {
                        return false;
                    }
                }).collect(Collectors.toSet());
        Assertions.assertEquals(collect.size(), 2);
        circuitBreakers.forEach(circuitBreaker -> {
            if (circuitBreaker.getName().contains(TestService1Client.class.getName())) {
                Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) DEFAULT_FAILURE_RATE_THRESHOLD);
                Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), DEFAULT_MINIMUM_NUMBER_OF_CALLS);
            } else if (circuitBreaker.getName().contains(TestService2Client.class.getName())) {
                Assertions.assertEquals((int) circuitBreaker.getCircuitBreakerConfig().getFailureRateThreshold(), (int) TEST_SERVICE_2_FAILURE_RATE_THRESHOLD);
                Assertions.assertEquals(circuitBreaker.getCircuitBreakerConfig().getMinimumNumberOfCalls(), TEST_SERVICE_2_MINIMUM_NUMBER_OF_CALLS);
            }
        });
    }

验证断路器是基于服务和方法打开的。

我们给 TestService1Client 添加一个方法:

@GetMapping("/status/500")
String testCircuitBreakerStatus500();

这个方法一定会调用失败,从而导致断路器打开。经过 2 次失败以上后(因为配置最少触发断路器打开的请求个数为 2),验证断路器状态:

@Test
public void testCircuitBreakerOpenBasedOnServiceAndMethod() {
    //防止断路器影响
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    AtomicBoolean passed = new AtomicBoolean(false);
    for (int i = 0; i < 10; i++) {
        //多次调用会导致断路器打开
        try {
            System.out.println(testService1Client.testCircuitBreakerStatus500());
        } catch(Exception e) {}
        List<CircuitBreaker> circuitBreakers = circuitBreakerRegistry.getAllCircuitBreakers().asJava();
        circuitBreakers.stream().filter(circuitBreaker -> {
            return circuitBreaker.getName().contains("testCircuitBreakerStatus500")
                    && circuitBreaker.getName().contains("TestService1Client");
        }).findFirst().ifPresent(circuitBreaker -> {
            //验证对应微服务和方法的断路器被打开
            if (circuitBreaker.getState().equals(CircuitBreaker.State.OPEN)) {
                passed.set(true);
                //断路器打开后,调用其他方法,不会抛出断路器打开异常
                testService1Client.testAnything();
            }
        });
    }

    Assertions.assertTrue(passed.get());
}

这样,我们就成功验证了,验证断路器是基于服务和方法打开的。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

36. 验证断路器正确性

点赞
收藏
评论区
推荐文章

暂无数据

unknown
unknown
Lv1
男 · rrrr · rrrrrrrr
rrrrr
文章
0
粉丝
17
获赞
0
热门文章

暂无数据