本系列代码地址: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. Python代码阅读(第8篇):列表元素逻辑判断

    Python 代码阅读合集介绍:为什么不推荐Python初学者直接看项目源码 本篇阅读的三份代码的功能分别是判断列表中的元素是否都符合给定的条件:判断列表中是否存在符合给定的条件的元素:以及判断列表中 ...

  2. join方法个人理解

    首先抛出对join的疑问 如果我有一个a线程,一个b线程 那此时 a.start(); b.start(); a.join(); b.join(); 是否意思是a线程先执行完,然后再执行b线程; 如果 ...

  3. 深度学习|基于LSTM网络的黄金期货价格预测--转载

    深度学习|基于LSTM网络的黄金期货价格预测 前些天看到一位大佬的深度学习的推文,内容很适用于实战,争得原作者转载同意后,转发给大家.之后会介绍LSTM的理论知识. 我把code先放在我github上 ...

  4. logback日志入门超级详细讲解

    基本信息 日志:就是能够准确无误地把系统在运行状态中所发生的情况描述出来(连接超时.用户操作.异常抛出等等): 日志框架:就是集成能够将日志信息统一规范后输出的工具包. Logback优势 Logba ...

  5. 利用Conda尝鲜Python 3.10

    1 简介 就在几天前,Python3.10的第一个正式版本3.10.0发布,之前我们只是从其各个测试版本中捕风捉影地知晓了一些可能加入的新特性,而在正式版本中,我们得以一睹其正式加入的诸多新特性. 本 ...

  6. Serverless:这真的是未来吗?(二)

    原文 | https://www.pulumi.com/blog/is_serverless_the_future_part_2/ 作者 | Lee Briggs & Piers Karsen ...

  7. Python | JSON 数据解析(Json & JsonPath)

    一.什么是JSON? JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式.它基于 ECMAScript (欧洲计算机协会制定的js规范)的一 ...

  8. python OptionParser的用法

    from optparse import OptionParser parser = OptionParser(usage = "usage: %prog [options] arg&quo ...

  9. 微服务网关Ocelot加入IdentityServer4鉴权-.NetCore(.NET5)中使用

    Consul+Ocelot+Polly在.NetCore中使用(.NET5)-Consul服务注册,服务发现 Consul+Ocelot+Polly在.NetCore中使用(.NET5)-网关Ocel ...

  10. SharkCTF2021 Classic_Crypto_king2

    crypto类题. 题面如下: 前面的代码给出了原理:后面的字符串第一行是print出的key,第二行是密文. 加密原理是,首先对table进行乱序处理,然后将明文flag按照(顺序table--&g ...