本系列代码地址: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

SpringCloud升级之路2020.0.x版-35. 验证线程隔离正确性的更多相关文章

  1. SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在前面一节,我们利用 resilience4j 粘合了 OpenFeign 实现了断路器. ...

  2. SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(2)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节针对我们的重试进行测试 验证针对限流器异常的重试正确 通过系列前面的源码分析 ...

  3. SpringCloud升级之路2020.0.x版-34.验证重试配置正确性(3)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 我们继续上一节针对我们的重试进行测试 验证针对可重试的方法响应超时异常重试正确 我们可以通 ...

  4. SpringCloud升级之路2020.0.x版-36. 验证断路器正确性

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 上一节我们通过单元测试验证了线程隔离的正确性,这一节我们来验证我们断路器的正确性,主要包括 ...

  5. SpringCloud升级之路2020.0.x版-1.背景

    本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ Spring ...

  6. SpringCloud升级之路2020.0.x版-41. SpringCloudGateway 基本流程讲解(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 接下来,将进入我们升级之路的又一大模块,即网关模块.网关模块我们废弃了已经进入维护状态的 ...

  7. SpringCloud升级之路2020.0.x版-6.微服务特性相关的依赖说明

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford spring-cl ...

  8. SpringCloud升级之路2020.0.x版-10.使用Log4j2以及一些核心配置

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Log4 ...

  9. SpringCloud升级之路2020.0.x版-29.Spring Cloud OpenFeign 的解析(1)

    本系列代码地址:https://github.com/JoJoTec/spring-cloud-parent 在使用云原生的很多微服务中,比较小规模的可能直接依靠云服务中的负载均衡器进行内部域名与服务 ...

随机推荐

  1. IO流基本概念

    IO流主要分为两类 节点流:直接能够进行数据写入或读取的I0流.可以单独执行读写操作,但是功能比较单一,只能进行一些基本 的操作.例如:FileInputStream FileInputStream ...

  2. mybatis本地缓存&分布式缓存干货分享

    前言:干货记录学习mybatis实际开发中缓存的使用. 环境: springboot2.X + mybatis3.x Mybatis是一款持久层框架,它提供了一级缓存和二级缓存. 名词解释 一级缓存( ...

  3. Xamarin Android使用自签名证书

    背景 项目中后台web服务部署成https服务时,需要使用SSL证书,如果我们不使用公共的CA时,怎么办? 不仅如此,因为是小项目,App应用主要是小范围使用,此时只有IP地址,根本没有域名,怎么办? ...

  4. WPF进阶技巧和实战08-依赖属性与绑定02

    将元素绑定在一起 数据绑定最简单的形式是:源对象是WPF元素而且源属性是依赖项属性.依赖项属性内置了更改通知支持,当源对象中改变依赖项属性时,会立即更新目标对象的绑定属性. 元素绑定到元素也是经常使用 ...

  5. Java-多态(上)

    什么是多态 同一方法可以根据发送对象的不同而采取多种不同的行为方式 一个对象实际类型是确定的 但指向其引用类型却有很多 注意事项 多态是方法的多态 属性没有多态 父类和子类 有联系 类型转换异常 Cl ...

  6. C++学习 3 结构体

    结构体基本概念: 结构体属于用户自定义的数据类型,允许用户存储不同的数据类型: 结构体定义和使用: 语法:struct 结构体名 { 结构体成员列表 }: 通过结构体创建变量名的方式有三种:     ...

  7. HC(Histogram-based Contrast) 基于直方图对比度的显著性

    HC(Histogram-based Contrast) 基于直方图对比度的显著性 来源于: 2011, Global contrast based salient region detection, ...

  8. 市区择房分析(ArcPy实现)

    1, 背景 如何找到环境好.购物方便.小孩上学方便的居住区地段是购房者最关心的问题.因此购房者就需要从总体上对商品房的信息进行研究分析,选择最适宜的购房地段. 2,目的 学会利用缓冲区分析和叠置分析解 ...

  9. 个人记录:对于python学习的反思和总结(一)

    在写代码时,总是遇到写着写着不知道怎么写了的情况,或者无法把自己的想法用程序表达出来,所以有时候我们需要建立一个自己的编程思路,对一个具体程序的编程有一个比较清晰的想法:因此我把自己的思路总结了一下, ...

  10. 权限管理RBAC模型概述

    一.什么是RBAC模型 RBAC模型(Role-Based Access Control:基于角色的访问控制)模型是比较早期提出的权限实现模型,在多用户计算机时期该思想即被提出,其中以美国George ...