SpringCloud升級之路2020.0.x版-35. 驗證線程隔離正確性

本系列代碼地址: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.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
        //testService2Client 的線程池配置
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@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 testConfigureThreadPool() {
    //防止斷路器影響
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    //調用下這兩個 FeignClient 確保對應的 NamedContext 被初始化
    testService1Client.anything();
    testService2Client.anything();
    //驗證線程隔離的實際配置,符合我們的填入的配置
    ThreadPoolBulkhead threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
            .stream().filter(t -> t.getName().contains("service1Instance1")).findFirst().get();
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 10);
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 10);
    threadPoolBulkhead = threadPoolBulkheadRegistry.getAllBulkheads().asJava()
            .stream().filter(t -> t.getName().contains("service1Instance2")).findFirst().get();
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getCoreThreadPoolSize(), 5);
    Assertions.assertEquals(threadPoolBulkhead.getBulkheadConfig().getMaxThreadPoolSize(), 5);
}

相同微服務調用不同實例的時候,使用的是不同的線程(池)。

我們需要確保,最後調用(也就是發送 http 請求)的執行的線程池,必須是對應的 ThreadPoolBulkHead 中的線程池。這個需要我們對 ApacheHttpClient 做切面實現,添加註解 @EnableAspectJAutoProxy(proxyTargetClass = true)

//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.thread-pool-bulkhead.configs.default.coreThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.maxThreadPoolSize=10",
        "resilience4j.thread-pool-bulkhead.configs.default.queueCapacity=1" ,
        //testService2Client 的線程池配置
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.coreThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.maxThreadPoolSize=5",
        "resilience4j.thread-pool-bulkhead.configs.testService2Client.queueCapacity=1",
})
@Log4j2
public class OpenFeignClientTest {
    @SpringBootApplication
    @Configuration
    @EnableAspectJAutoProxy(proxyTargetClass = true)
    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;
        }
    }
}

攔截 ApacheHttpClientexecute 方法,這樣可以拿到真正負責 http 調用的線程池,將線程其放入請求的 Header:

@Aspect
public static class ApacheHttpClientAop {
    //在最後一步 ApacheHttpClient 切面
    @Pointcut("execution(* com.github.jojotech.spring.cloud.webmvc.feign.ApacheHttpClient.execute(..))")
    public void annotationPointcut() {
    }

    @Around("annotationPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //設置 Header,不能通過 Feign 的 RequestInterceptor,因爲我們要拿到最後調用 ApacheHttpClient 的線程上下文
        Request request = (Request) pjp.getArgs()[0];
        Field headers = ReflectionUtils.findField(Request.class, "headers");
        ReflectionUtils.makeAccessible(headers);
        Map<String, Collection<String>> map = (Map<String, Collection<String>>) ReflectionUtils.getField(headers, request);
        HashMap<String, Collection<String>> stringCollectionHashMap = new HashMap<>(map);
        stringCollectionHashMap.put(THREAD_ID_HEADER, List.of(String.valueOf(Thread.currentThread().getName())));
        ReflectionUtils.setField(headers, request, stringCollectionHashMap);
        return pjp.proceed();
    }
}

這樣,我們就能拿到具體承載請求的線程的名稱,從名稱中可以看出他所處於的線程池(格式爲“bulkhead-線程隔離名稱-n”,例如 bulkhead-testService1Client:www.httpbin.org:80-1),接下來我們就來看下不同的實例是否用了不同的線程池進行調用:

@Test
public void testDifferentThreadPoolForDifferentInstance() throws InterruptedException {
    //防止斷路器影響
    circuitBreakerRegistry.getAllCircuitBreakers().asJava().forEach(CircuitBreaker::reset);
    Set<String> threadIds = Sets.newConcurrentHashSet();
    Thread[] threads = new Thread[100];
    //循環100次
    for (int i = 0; i < 100; i++) {
        threads[i] = new Thread(() -> {
            Span span = tracer.nextSpan();
            try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
                HttpBinAnythingResponse response = testService1Client.anything();
                //因爲 anything 會返回我們發送的請求實體的所有內容,所以我們能獲取到請求的線程名稱 header
                String threadId = response.getHeaders().get(THREAD_ID_HEADER);
                threadIds.add(threadId);
            }
        });
        threads[i].start();
    }
    for (int i = 0; i < 100; i++) {
        threads[i].join();
    }
    //確認實例 testService1Client:httpbin.org:80 線程池的線程存在
    Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:httpbin.org:80")));
    //確認實例 testService1Client:httpbin.org:80 線程池的線程存在
    Assertions.assertTrue(threadIds.stream().anyMatch(s -> s.contains("testService1Client:www.httpbin.org:80")));
}

這樣,我們就成功驗證了,實例調用的線程池隔離。

微信搜索“我的編程喵”關注公衆號,每日一刷,輕鬆提升技術,斬獲各種offer

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章