SpringCloud升級之路2020.0.x版-34.驗證重試配置正確性(2)

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

本系列程式碼地址:https://github.com/JoJoTec/spring-cloud-parent

我們繼續上一節針對我們的重試進行測試

驗證針對限流器異常的重試正確

通過系列前面的原始碼分析,我們知道 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 testRetryOnBulkheadException() {
    //防止斷路器影響
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    this.testService1Client.anything();
    ThreadPoolBulkhead threadPoolBulkhead;
    try {
        threadPoolBulkhead = threadPoolBulkheadRegistry
                .bulkhead("testService1Client:httpbin.org:80", "testService1Client");
    } catch (ConfigurationNotFoundException e) {
        //找不到就用預設配置
        threadPoolBulkhead = threadPoolBulkheadRegistry
                .bulkhead("testService1Client:httpbin.org:80");
    }
    //執行緒佇列我們配置的是 1,執行緒池大小是 10,這樣會將執行緒池填充滿
    for (int i = 0; i < 10 + 1; i++) {
        threadPoolBulkhead.submit(() -> {
            try {
                //這樣任務永遠不會結束了
                Thread.currentThread().join();
            }
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
    }
    //呼叫多次,呼叫成功即對斷路器異常重試了
    for (int i = 0; i < 10; i++) {
        this.testService1Client.anything();
    }
}

執行測試,日誌中可以看出,針對執行緒池滿的異常進行重試了:

2021-11-13 03:35:16.371  INFO [,,] 3824 --- [           main] c.g.j.s.c.w.f.DefaultErrorDecoder        : TestService1Client#anything() response: 584-Bulkhead 'testService1Client:httpbin.org:80' is full and does not permit further calls, should retry: true

驗證針對非 2xx 響應碼可重試的方法重試正確

我們通過使用 http.bin 的 /status/{statusCode} 介面,這個介面會根據路徑引數 statusCode 返回對應狀態碼的響應:

@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
    @GetMapping("/status/500")
    String testGetRetryStatus500();
}

我們如何感知被重試三次呢?每次呼叫,就會從負載均衡器獲取一個服務例項。在負載均衡器程式碼中,我們使用了根據當前 sleuth 的上下文的 traceId 的快取,每次呼叫,traceId 對應的 position 值就會加 1。我們可以通過觀察這個值的變化獲取到究竟本次請求呼叫了幾次負載均衡器,也就是做了幾次呼叫。

編寫測試:

@Test
public void testNon2xxRetry() {
    Span span = tracer.nextSpan();
    try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
        //防止斷路器影響
        circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
        long l = span.context().traceId();
        RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
                = (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
        AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
        int start = atomicInteger.get();
        try {
            //get 方法會重試
            testService1Client.testGetRetryStatus500();
        } catch (Exception e) {
        }
        //因為每次呼叫都會失敗,所以會重試配置的 3 次
        Assertions.assertEquals(3, atomicInteger.get() - start);
    }
}

驗證針對非 2xx 響應碼不可重試的方法沒有重試

我們通過使用 http.bin 的 /status/{statusCode} 介面,這個介面會根據路徑引數 statusCode 返回對應狀態碼的響應,並且支援各種 HTTP 請求方式:

@FeignClient(name = "testService1", contextId = "testService1Client")
public interface TestService1Client {
    @PostMapping("/status/500")
    String testPostRetryStatus500();
}

預設情況下,我們只會對 GET 方法重試,對於其他 HTTP 請求方法,是不會重試的:

@Test
public void testNon2xxRetry() {
    Span span = tracer.nextSpan();
    try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
        //防止斷路器影響
        circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
        long l = span.context().traceId();
        RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance
                = (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService1");
        AtomicInteger atomicInteger = loadBalancerClientFactoryInstance.getPositionCache().get(l);
        int start = atomicInteger.get();
        try {
            //post 方法不會重試
            testService1Client.testPostRetryStatus500();
        } catch (Exception e) {
        }
        //不會重試,因此只會被呼叫 1 次
        Assertions.assertEquals(1, atomicInteger.get() - start);
    }
}

微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer

相關文章