Spring cloud upgrade 2020.0.x version - 33. Implementation of retry, circuit breaker and thread isolation source code

Code address of this series: https://github.com/JoJoTec/sp...In the previous two sections, we combed the ideas of realiz...

Code address of this series: https://github.com/JoJoTec/sp...

In the previous two sections, we combed the ideas of realizing Feign circuit breaker and thread isolation, and explained how to optimize the current load balancing algorithm. However, how to update the data cache of load balancing and the source code of retry, circuit breaker and thread isolation have not been mentioned. We will analyze it in detail in this section.

First, from spring.factories, add the loading of our custom OpenFeign configuration:

spring.factories

# AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.github.jojotech.spring.cloud.webmvc.auto.OpenFeignAutoConfiguration

The automatic configuration class is OpenFeignAutoConfiguration. Its contents are:

OpenFeignAutoConfiguration.java

//Set ` @ Configuration(proxyBeanMethods=false) `, because there are no @ Bean methods calling each other. You need to return the same Bean every time. There is no need to close the agent to increase the startup speed @Configuration(proxyBeanMethods = false) //Loading configuration, CommonOpenFeignConfiguration @Import(CommonOpenFeignConfiguration.class) //Enable OpenFeign annotation scanning and configuration. The default configuration is DefaultOpenFeignConfiguration. In fact, the default configuration class of Feign's NamedContextFactory (i.e. FeignContext) is DefaultOpenFeignConfiguration @EnableFeignClients(value = "com.github.jojotech", defaultConfiguration = DefaultOpenFeignConfiguration.class) public class OpenFeignAutoConfiguration { }

Why add this layer instead of directly using the CommonOpenFeignConfiguration of Import? Use @ AutoConfigurationBefore and @ AutoConfigurationAfter to configure the sequence before and after loading and other autoconfigurations@ AutoConfigurationBefore and @ AutoConfigurationAfter are spring boot annotations that only take effect for AutoConfiguration loaded by spring.factories. Therefore, this layer should be added to the design to prevent us from using these annotations in the future.

Commonopenfeign configuration contains some beans shared by all OpenFeign. These beans are shared by all feign clients in a single instance, including:

  1. FeignClient is the underlying HTTP Client of the Client to be used. Here we use Apache HttpClient
  2. Encapsulate Apache HttpClient into the Apache HttpClient of the Client to be used by FeignClient
  3. FeignBlockingLoadBalancerClient is the core class of load balancing implementation for FeignClient of spring cloud openfeign. We need to encapsulate the agent to realize circuit breaker and thread isolation and load balancing data collection. The encapsulated class is FeignBlockingLoadBalancerClientDelegate implemented by ourselves. The core class that implements the circuit breaker and thread isolation logic is Resilience4jFeignClient.

CommonOpenFeignConfiguration.java

@Configuration(proxyBeanMethods = false) public class CommonOpenFeignConfiguration { //Create Apache HttpClient and customize some configurations @Bean public HttpClient getHttpClient() { // Long connection for 5 minutes PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(5, TimeUnit.MINUTES); // Total connections pollingConnectionManager.setMaxTotal(1000); // Concurrent number of routes pollingConnectionManager.setDefaultMaxPerRoute(1000); HttpClientBuilder httpClientBuilder = HttpClients.custom(); httpClientBuilder.setConnectionManager(pollingConnectionManager); // To maintain the long connection configuration, you need to add keep alive in the header httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy()); return httpClientBuilder.build(); } //Create a Bean using the Client interface of OpenFeign implemented by HttpClient @Bean public ApacheHttpClient apacheHttpClient(HttpClient httpClient) { return new ApacheHttpClient(httpClient); } //FeignBlockingLoadBalancerClient's proxy class is also a Bean that implements OpenFeign's Client interface @Bean //Use the Primary node to make FeignBlockingLoadBalancerClientDelegate the Bean actually used by all feignclients @Primary public FeignBlockingLoadBalancerClientDelegate feignBlockingLoadBalancerCircuitBreakableClient( ServiceInstanceMetrics serviceInstanceMetrics, //The Apache httpclient bean we created above ApacheHttpClient apacheHttpClient, //Why use ObjectProvider? Please refer to the notes of FeignBlockingLoadBalancerClientDelegate source code ObjectProvider<LoadBalancerClient> loadBalancerClientProvider, //Thread isolation for resilience4j ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry, //Circuit breaker for resilience4j CircuitBreakerRegistry circuitBreakerRegistry, //Tracer of Sleuth, which is used to obtain the request context Tracer tracer, //Load balancing properties LoadBalancerProperties properties, //Why not use FeignBlockingLoadBalancerClient directly? Please refer to the notes of FeignBlockingLoadBalancerClientDelegate LoadBalancerClientFactory loadBalancerClientFactory ) { return new FeignBlockingLoadBalancerClientDelegate( //Our own encapsulated core Client implementation adds circuit breaker, thread isolation and load balancing data collection new Resilience4jFeignClient( serviceInstanceMetrics, apacheHttpClient, threadPoolBulkheadRegistry, circuitBreakerRegistry, tracer ), loadBalancerClientProvider, properties, loadBalancerClientFactory ); } }

Resilience4jFeignClient glues the circuit breaker, the core code of thread isolation, and also records the actual call data of load balancing

Resilience4jFeignClient.java

public class Resilience4jFeignClient implements Client { private final ServiceInstanceMetrics serviceInstanceMetrics; private final ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry; private final CircuitBreakerRegistry circuitBreakerRegistry; private final Tracer tracer; private ApacheHttpClient apacheHttpClient; public Resilience4jFeignClient( ServiceInstanceMetrics serviceInstanceMetrics, ApacheHttpClient apacheHttpClient, ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry, CircuitBreakerRegistry circuitBreakerRegistry, Tracer tracer ) { this.serviceInstanceMetrics = serviceInstanceMetrics; this.apacheHttpClient = apacheHttpClient; this.threadPoolBulkheadRegistry = threadPoolBulkheadRegistry; this.circuitBreakerRegistry = circuitBreakerRegistry; this.tracer = tracer; } @Override public Response execute(Request request, Request.Options options) throws IOException { //Gets the FeignClient annotation of the interface that defines FeignClient FeignClient annotation = request.requestTemplate().methodMetadata().method().getDeclaringClass().getAnnotation(FeignClient.class); //Consistent with Retry, use contextId instead of microservice name //contextId will be used as the key to read the circuit breaker and thread isolation configuration later String contextId = annotation.contextId(); //Get instance unique id String serviceInstanceId = getServiceInstanceId(contextId, request); //Get instance + method unique id String serviceInstanceMethodId = getServiceInstanceMethodId(contextId, request); ThreadPoolBulkhead threadPoolBulkhead; CircuitBreaker circuitBreaker; try { //One thread pool per instance threadPoolBulkhead = threadPoolBulkheadRegistry.bulkhead(serviceInstanceId, contextId); } catch (ConfigurationNotFoundException e) { threadPoolBulkhead = threadPoolBulkheadRegistry.bulkhead(serviceInstanceId); } try { //Each service instance specific method has a resilience4j fuse recorder, which fuses in the service instance specific method dimension. All the service instance specific methods share the resilience4j fuse configuration of the service circuitBreaker = circuitBreakerRegistry.circuitBreaker(serviceInstanceMethodId, contextId); } catch (ConfigurationNotFoundException e) { circuitBreaker = circuitBreakerRegistry.circuitBreaker(serviceInstanceMethodId); } //Keep traceId Span span = tracer.currentSpan(); ThreadPoolBulkhead finalThreadPoolBulkhead = threadPoolBulkhead; CircuitBreaker finalCircuitBreaker = circuitBreaker; Supplier<CompletionStage<Response>> completionStageSupplier = ThreadPoolBulkhead.decorateSupplier(threadPoolBulkhead, OpenfeignUtil.decorateSupplier(circuitBreaker, () -> { try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) { log.info("call url: {} -> {}, ThreadPoolStats({}): {}, CircuitBreakStats({}): {}", request.httpMethod(), request.url(), serviceInstanceId, JSON.toJSONString(finalThreadPoolBulkhead.getMetrics()), serviceInstanceMethodId, JSON.toJSONString(finalCircuitBreaker.getMetrics()) ); Response execute = apacheHttpClient.execute(request, options); log.info("response: {} - {}", execute.status(), execute.reason()); return execute; } catch (IOException e) { throw new CompletionException(e); } }) ); ServiceInstance serviceInstance = getServiceInstance(request); try { serviceInstanceMetrics.recordServiceInstanceCall(serviceInstance); Response response = Try.ofSupplier(completionStageSupplier).get().toCompletableFuture().join(); serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, true); return response; } catch (BulkheadFullException e) { //Thread pool current limit exception serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, false); return Response.builder() .request(request) .status(SpecialHttpStatus.BULKHEAD_FULL.getValue()) .reason(e.getLocalizedMessage()) .requestTemplate(request.requestTemplate()).build(); } catch (CompletionException e) { serviceInstanceMetrics.recordServiceInstanceCalled(serviceInstance, false); //All exceptions thrown internally are encapsulated with a layer of CompletionException, so you need to take out the Exception inside Throwable cause = e.getCause(); //For circuit breaker opening, the corresponding special error code is returned if (cause instanceof CallNotPermittedException) { return Response.builder() .request(request) .status(SpecialHttpStatus.CIRCUIT_BREAKER_ON.getValue()) .reason(cause.getLocalizedMessage()) .requestTemplate(request.requestTemplate()).build(); } //For IOException, you need to judge whether the request has been sent //For the exception of connect time out, you can try again because the request is not sent out, but for example, read time out cannot because the request has been sent out if (cause instanceof IOException) { boolean containsRead = cause.getMessage().toLowerCase().contains("read"); if (containsRead) { log.info("{}-{} exception contains read, which indicates the request has been sent", e.getMessage(), cause.getMessage()); //If it is a read exception, it means that the request has been sent and cannot be retried (unless it is a GET request or has a RetryableMethod annotation, which is determined by the DefaultErrorDecoder) return Response.builder() .request(request) .status(SpecialHttpStatus.NOT_RETRYABLE_IO_EXCEPTION.getValue()) .reason(cause.getLocalizedMessage()) .requestTemplate(request.requestTemplate()).build(); } else { return Response.builder() .request(request) .status(SpecialHttpStatus.RETRYABLE_IO_EXCEPTION.getValue()) .reason(cause.getLocalizedMessage()) .requestTemplate(request.requestTemplate()).build(); } } throw e; } } private ServiceInstance getServiceInstance(Request request) throws MalformedURLException { URL url = new URL(request.url()); DefaultServiceInstance defaultServiceInstance = new DefaultServiceInstance(); defaultServiceInstance.setHost(url.getHost()); defaultServiceInstance.setPort(url.getPort()); return defaultServiceInstance; } //Get the microservice instance id in the format of FeignClient's contextId:host:port, for example: test1Client:10.238.45.78:8251 private String getServiceInstanceId(String contextId, Request request) throws MalformedURLException { //Resolve URL URL url = new URL(request.url()); //Splice microservice instance id return contextId + ":" + url.getHost() + ":" + url.getPort(); } //Get the microservice instance method id in the format of contextId:host:port:methodName of FeignClient, for example: test1Client:10.238.45.78:8251: private String getServiceInstanceMethodId(String contextId, Request request) throws MalformedURLException { URL url = new URL(request.url()); //Obtain the unique id through the way of microservice name + instance + method String methodName = request.requestTemplate().methodMetadata().method().toGenericString(); return contextId + ":" + url.getHost() + ":" + url.getPort() + ":" + methodName; } }

In the above, we defined several special HTTP return codes. The main purpose is to encapsulate some exceptions into response returns, and then decode them into a unified RetryableException through our Feign error decoder. In this way, in the retry configuration of resilience4j, we do not need to configure very complex exception retries, but only retry for RetryableException

We want to make the core load balance of spring-cloud-openfeign Client. After completing the call LoadBalancer to select instances and replace url, the client invoked directly is ApacheHttpClient, but we are the above class, so FeignBlockingLoadBalancerClientDelegate package is added:

/** * LoadBalancerClient is required to initialize FeignBlockingLoadBalancerClient * However, since Spring Cloud LoadBalancer BlockingClient is loaded after Spring Cloud 2020, the order is forced to be added * @see org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration * This automatic configuration adds @ autoconfiguraeafter (loadbalancenautoconfiguration. Class) * As a result, we cannot get BlockingClient when initializing FeignClient * Therefore, the LoadBalancerClient needs to be encapsulated through the ObjectProvider. When FeignClient is really called, get the LoadBalancerClient through the ObjectProvider to create FeignBlockingLoadBalancerClient */ public class FeignBlockingLoadBalancerClientDelegate implements Client { private FeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient; private final Client delegate; private final ObjectProvider<LoadBalancerClient> loadBalancerClientObjectProvider; private final LoadBalancerProperties properties; private final LoadBalancerClientFactory loadBalancerClientFactory; public FeignBlockingLoadBalancerClientDelegate( Client delegate, ObjectProvider<LoadBalancerClient> loadBalancerClientObjectProvider, LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory ) { this.delegate = delegate; this.loadBalancerClientObjectProvider = loadBalancerClientObjectProvider; this.properties = properties; this.loadBalancerClientFactory = loadBalancerClientFactory; } @Override public Response execute(Request request, Request.Options options) throws IOException { if (feignBlockingLoadBalancerClient == null) { synchronized (this) { if (feignBlockingLoadBalancerClient == null) { feignBlockingLoadBalancerClient = new FeignBlockingLoadBalancerClient( this.delegate, this.loadBalancerClientObjectProvider.getIfAvailable(), this.properties, this.loadBalancerClientFactory ); } } } return feignBlockingLoadBalancerClient.execute(request, options); } }

The default configuration of the NamedContextFactory (i.e. FeignContext) of FeignClient specified by us, DefaultOpenFeignConfiguration, mainly adheres to the retry logic and error decoder:

@Configuration(proxyBeanMethods = false) public class DefaultOpenFeignConfiguration { @Bean public ErrorDecoder errorDecoder() { return new DefaultErrorDecoder(); } @Bean public Feign.Builder resilience4jFeignBuilder( List<FeignDecoratorBuilderInterceptor> feignDecoratorBuilderInterceptors, FeignDecorators.Builder builder ) { feignDecoratorBuilderInterceptors.forEach(feignDecoratorBuilderInterceptor -> feignDecoratorBuilderInterceptor.intercept(builder)); return Resilience4jFeign.builder(builder.build()); } @Bean public FeignDecorators.Builder defaultBuilder(Environment environment, RetryRegistry retryRegistry) { String name = environment.getProperty("feign.client.name"); Retry retry = null; try { retry = retryRegistry.retry(name, name); } catch (ConfigurationNotFoundException e) { retry = retryRegistry.retry(name); } //Override the exception judgment and retry only for feign.RetryableException. All exceptions that need to be retried are encapsulated as RetryableException in DefaultErrorDecoder and Resilience4jFeignClient retry = Retry.of(name, RetryConfig.from(retry.getRetryConfig()).retryOnException(throwable -> { return throwable instanceof feign.RetryableException; }).build()); return FeignDecorators.builder().withRetry( retry ); } }

The error decoder encapsulates the above retrieable exception response code and the request we want to retry into a RetryableException. The code will not be repeated. In this way, we have implemented a custom FeignClient that implements retry, circuit breaker and thread isolation. It can be configured as follows:

application.yml configuration:

################ feign to configure ################ feign: hystrix: enabled: false client: config: default: # Link timeout connectTimeout: 500 # Read timeout readTimeout: 8000 test1-client: # Link timeout connectTimeout: 500 # Read timeout readTimeout: 60000 ################ resilience to configure ################ resilience4j.circuitbreaker: configs: default: registerHealthIndicator: true slidingWindowSize: 10 minimumNumberOfCalls: 5 slidingWindowType: TIME_BASED permittedNumberOfCallsInHalfOpenState: 3 automaticTransitionFromOpenToHalfOpenEnabled: true waitDurationInOpenState: 2s failureRateThreshold: 30 eventConsumerBufferSize: 10 recordExceptions: - java.lang.Exception resilience4j.retry: configs: default: maxRetryAttempts: 2 test1-client: maxRetryAttempts: 3 resilience4j.thread-pool-bulkhead: configs: default: maxThreadPoolSize: 64 coreThreadPoolSize: 32 queueCapacity: 32

Define Feignclient:

//This will use all configurations where the key is test1 client. If there is no test1 client in the corresponding configuration, use default @FeignClient(name = "service1", contextId = "test1-client") public interface TestService1Client { @GetMapping("/anything") HttpBinAnythingResponse anything(); }
//All configurations where the key is test2 client will be used. Since there is no separate configuration of test2 client here, all default configurations are used @FeignClient(name = "service1", contextId = "test2-client") public interface TestService1Client2 { @GetMapping("/anything") HttpBinAnythingResponse anything(); }

At the beginning of the next section, we will unit test the FeignClient package implemented here to verify our correctness.

WeChat search "my programming meow" attention to the official account, daily brush, easy to upgrade technology, and capture all kinds of offer:

12 November 2021, 16:01 | Views: 6365

Add new comment

For adding a comment, please log in
or create account

0 comments