SpringCloud升級之路2020.0.x版-36. 驗證斷路器正確性

乾貨滿滿張雜湊發表於2021-11-18

本系列程式碼地址: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

相關文章