SpringCloud升级之路2020.0.x版-35. 验证线程隔离正确性

SpringCloud升级之路2020.0.x版-35. 验证线程隔离正确性

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

  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",})@Log4j2public 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;        }    }}

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

@Testpublic 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",})@Log4j2public 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;        }    }}

拦截 ApacheHttpClient 的 execute 方法,这样可以拿到真正负责 http 调用的线程池,将线程其放入请求的 Header:

@Aspectpublic 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),接下来我们就来看下不同的实例是否用了不同的线程池进行调用:

@Testpublic 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")));}

这样,我们就成功验证了,实例调用的线程池隔离。

内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/99871.html

(0)

相关推荐

  • 胎儿畸形ppt,检查胎儿畸形

    随着人们对优生优育的重视,产科超声备受关注。对于胎儿形态上的发育异常,可以说超声是首当其冲的检查手段,也责任重大,产筛医生如履薄冰。

    生活 2021年11月14日
  • 雅虎是阿里巴巴最大股东,雅虎持有阿里巴巴多少股权

    前言阿里巴巴如今已经是影响中国,乃至影响世界的互联网巨头了,所以它的一举一动,特别是股权变动信息,尤其引人注目。

    科技 2021年10月22日
  • 诺贝尔文学奖绘本值得买吗(诺贝尔文学奖绘本)

    前几天偶然读到一段话,完美地回答了一个老生常谈的问题,“读书的意义到底是什么?”那段话是这样说的:“当我还是个孩子时,我吃过很多食物,大部分已经一去不复返而被我忘掉了,现在已经记不起来吃过什么了。但可以肯定的是,他们中的一部分已经长成了我的骨头和肉。阅读对我的改变也是如此。”诚然,在孩子的童年时期,让他们多读书、读好书,能为他们未来人格、能力和三观的塑造打下坚实基础。让孩子阅读,就是在给予他们结实强壮、丰盈饱满的「骨头和肉」。©公众号图库可市面上的书籍千千万,什么是适合、值得孩子读的「好书」?有个简单的选择方法,就是「读经典」。经过漫长的时间和变化的时代,依然能被读者们追捧、喜爱,经典的文学作品一定是有永不过时的魅力的。如果要在经典中找经典来读,那“诺贝尔文学奖”当之无愧。因为它是目前世界上公认的具有高度影响力的文学类奖项,获奖作家无不经过时间的检验和岁月的历练。他们作品中也往往饱含人类智慧的光芒和亘久不衰的生命哲理,随便买一本他们的作品,基本上都不会出错。只不过,大部分诺奖作品,都因为内容和思想高深、广博,对于低龄儿童来说理解十分困难,基本要等到孩子上了初中、高中才会列入他们的书单。在这种情况下,如何早早地让孩子在幼儿园、小学时期就能接受诺奖文学经典之作的熏陶?现在我们终于等到了国内这套专为孩子定制的诺贝尔文学奖绘本——

    生活 2021年12月21日
  • 早上起床就吃早餐对身体好吗,早上到底要不要吃早餐

    在我们的一天饮食生活当中,最重要的就是早餐,但是越来越多的年轻人会忽视早餐的重要性,随着现在网络时代的不断发展,越来越多的年轻人,总是下班之后或者放了学之后晚上看手机看到很晚,早上起来根本就没有时间吃早饭,总喜欢熬夜,长此以往,会对身体造成一定的影响。

    2021年12月15日