上一節我們通過單元測試驗證了重試的正確性,這一節我們來驗證我們執行緒隔離的正確性,主要包括:
- 驗證配置正確載入:即我們在 Spring 配置(例如
application.yml
)中的加入的 Resilience4j 的配置被正確載入應用了。 - 相同微服務呼叫不同例項的時候,使用的是不同的執行緒(池)。
驗證配置正確載入
與之前驗證重試類似,我們可以定義不同的 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;
}
}
}
攔截 ApacheHttpClient
的 execute
方法,這樣可以拿到真正負責 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: