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

36. 验证断路器正确性

unknown
• 阅读 642

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
热门文章

暂无数据