Spring Cloud升級之路 - Hoxton - 4. 使用Resilience4j實現實例級別的隔離與熔斷

如何不啓用Hystrix

由於我們的入口註解類從@SpringCloudApplication替換成了SpringBootApplication,這樣不會啓用Spring-Cloud-CircuitBreaker。引入的Hystrix依賴也就沒有效果。請參考本系列第二節: Spring Cloud升級之路 - Hoxton - 2.入口類註解修改與OpenFeign的改造

使用Resilience4j實現實例級別的隔離與熔斷

爲什麼需要實例級別的熔斷呢?因爲某個微服務可能某些實例暫時不可用,我們希望在重試的時候,暫時不再重試這些實例。默認的Spring-Cloud-CircuitBreaker一般實現了微服務級別的熔斷,某個微服務某些實例暫時不可用但是某些實例可用的時候,就很有可能會發生整個微服務的熔斷。一般在滾動發佈的時候,如果操作不當,微服務級別的熔斷導致這個微服務不可用,但是實際上某些實例是可用的。所以,我們需要實例級別的熔斷,而不是微服務級別的。

爲什麼需要實例級別的線程隔離呢?防止某個實例發生問題,響應慢,阻塞了整個業務線程。

Spring-Cloud-CircuitBreaker裏面的實現對於resilience4j的功能使用有限,我們想利用其更多的功能(例如線程隔離等等)。而且,Spring-Cloud-CircuitBreaker可以直接用來實現微服務級別的熔斷,但是很難實現實例級別的熔斷。主要原因是他的配置是根據微服務名稱配置的,並且沒有擴展,導致我們想實現的話,修改代碼的地方太多了。所以我們捨棄了Spring-Cloud-CircuitBreaker

比較幸運的是,resilience4j官方有實現自己的spring-cloud-starter,裏面實現了他的所有功能的核心bean配置,很好用。我們採用這個starter以及相關的配置方式來實現我們的實例級別的隔離與熔斷。

在引入

<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-cloud2</artifactId>
    <version>${resilience4j-spring-cloud2.version}</version>
</dependency>

之後,會自動加載BulkheadRegistryThreadPoolBulkheadRegistryCircuitBreakerRegistryRateLimiterRegistryRetryRegistry這幾個Bean,配置這些Bean的分別是通過:

  • io.github.resilience4j.bulkhead.autoconfigure.BulkheadProperties: 前綴resilience4j.bulkhead
  • io.github.resilience4j.bulkhead.autoconfigure.ThreadPoolBulkheadProperties:前綴resilience4j.thread-pool-bulkhead
  • io.github.resilience4j.circuitbreaker.autoconfigure.CircuitBreakerProperties: 前綴resilience4j.circuitbreaker
  • io.github.resilience4j.ratelimiter.autoconfigure.RateLimiterProperties:前綴resilience4j.ratelimiter
  • io.github.resilience4j.retry.autoconfigure.RetryProperties: 前綴resilience4j.retry

這裏主要用到的元素是:CircuitBreakerThreadPoolBulkheadCircuitBreaker用來實現實例級別的熔斷,ThreadPoolBulkhead用來實現實例級別的線程隔離。

如何配置以及如何使用

CircuitBreaker相關的配置:CircuitBreaker

CircuitBreaker有五種狀態:CLOSED,OPEN 還有HALF_OPEN。剩下的兩種狀態是人爲操作,我們這裏不會用到:DISABLED還有
FORCED_OPEN. CLOSED 代表斷路器關閉,請求照常處理。OPEN 代表斷路器打開,如果有請求會拋出異常:CallNotPermittedException

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jNL6sKBU-1591348711197)(https://zhxhash-blog.oss-cn-beijing.aliyuncs.com/spring-cloud%E5%8D%87%E7%BA%A7%E4%B9%8B%E8%B7%AF/hoxton/39cdd54-state_machine.jpg)]

CircuitBreaker使用滑動窗口統計成功失敗的請求,並打開或者關閉斷路器。這個滑動窗口有兩種:

  • 基於計數的滑動窗口:使用一個大小爲 N 的環形數組,記錄最近 N 個請求結果。
  • 基於計時的滑動窗口:記錄最近 N 秒的請求結果
配置項 默認值 說明
failureRateThreshold 50 失敗請求百分比,超過這個比例,CircuitBreaker就會變成OPEN狀態
slowCallDurationThreshold 60000[ms] 慢調用時間,當一個調用慢於這個時間時,會被記錄爲慢調用
slowCallRateThreshold 100 當慢調用達到這個百分比的時候,CircuitBreaker就會變成OPEN狀態
permittedNumberOfCallsInHalfOpenState 10 CircuitBreaker處於HALF_OPEN狀態的時候,允許通過的請求數量
slidingWindowType COUNT_BASED 滑動窗口類型,COUNT_BASED代表是基於計數的滑動窗口,TIME_BASED代表是基於計時的滑動窗口
slidingWindowSize 100 滑動窗口大小,如果配置COUNT_BASED默認值100就代表是最近100個請求,如果配置TIME_BASED默認值100就代表是最近100s的請求。
minimumNumberOfCalls 100 最小請求個數。只有在滑動窗口內,請求個數達到這個個數,纔會觸發CircuitBreaker對於是否打開斷路器的判斷。
waitDurationInOpenState 60000[ms] OPEN狀態變成HALF_OPEN狀態需要的等待時間
automaticTransitionFromOpenToHalfOpenEnabled false 如果設置爲true代表是否自動從OPEN狀態變成HALF_OPEN,即使沒有請求過來。
recordExceptions empty 異常名單,指定一個 Exception 的 list,所有這個集合中的異常或者這些異常的子類,在調用的時候被拋出,都會被記錄爲失敗。其他異常不會被認爲是失敗,或者在 ignoreExceptions 中配置的異常也不會被認爲是失敗。默認是所有異常都認爲是失敗。
ignoreExceptions empty 異常白名單,在這個名單中的所有異常及其子類,都不會認爲是請求失敗,就算在 recordExceptions 中配置了這些異常也沒用。默認白名單爲空。

我們這裏實現的默認配置是:

resilience4j.circuitbreaker:
  configs:
    default:
      # 是否向 Actuator 的 HealthIndicator 註冊
      registerHealthIndicator: true
      slidingWindowSize: 10
      minimumNumberOfCalls: 5
      slidingWindowType: TIME_BASED
      permittedNumberOfCallsInHalfOpenState: 3
      automaticTransitionFromOpenToHalfOpenEnabled: true
      waitDurationInOpenState: 2s
      failureRateThreshold: 30
      recordExceptions:
        - java.lang.Exception

以上配置代表,默認情況下,所有Exception以及其子類都認爲是失敗。滑動窗口採用基於計時的,並且記錄最近10秒的請求。觸發斷路器判斷必須在10秒內至少有5個請求,在失敗比例達到30%以上之後,斷路器變爲OPEN。斷路器OPEN之後,在2秒後自動轉化爲HALF_OPEN

ThreadPoolBulkhead相關的配置:Create and configure a ThreadPoolBulkhead

配置項 默認值 說明
maxThreadPoolSize Runtime.getRuntime().availableProcessors() 最大線程池大小
coreThreadPoolSize Runtime.getRuntime().availableProcessors() - 1 核心線程池大小
queueCapacity 100 隊列大小
keepAliveDuration 20[ms] 線程存活時間

我們這裏實現的默認配置是:

resilience4j.thread-pool-bulkhead:
  configs:
    default:
      maxThreadPoolSize: 50
      coreThreadPoolSize: 10
      queueCapacity: 1

與Open-Feign粘合

我們需要在FeignClient被調用,選取好要發送請求的實例之後,加入CircuitBreakerThreadPoolBulkhead。也就是,我們需要拿到本次請求調用的實例,以及微服務名稱,加載對應的CircuitBreakerThreadPoolBulkhead,包裝調用請求,之後執行調用。

FeignClient的核心實現,根據org.springframework.cloud.openfeign.loadbalancer.DefaultFeignLoadBalancerConfiguration可以知道是org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient

@Bean
@ConditionalOnMissingBean
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
	return new FeignBlockingLoadBalancerClient(new Client.Default(null, null),
			loadBalancerClient);
}

查看FeignBlockingLoadBalancerClient的源碼:

@Override
public Response execute(Request request, Request.Options options) throws IOException {
	final URI originalUri = URI.create(request.url());
	//微服務名稱
	String serviceId = originalUri.getHost();
	Assert.state(serviceId != null,
			"Request URI does not contain a valid hostname: " + originalUri);
	//從負載均衡器選擇一個實例
	ServiceInstance instance = loadBalancerClient.choose(serviceId);
	if (instance == null) {
		String message = "Load balancer does not contain an instance for the service "
				+ serviceId;
		if (LOG.isWarnEnabled()) {
			LOG.warn(message);
		}
		return Response.builder().request(request)
				.status(HttpStatus.SERVICE_UNAVAILABLE.value())
				.body(message, StandardCharsets.UTF_8).build();
	}
	//修改原url
	String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
			.toString();
	//構建新的Request
	Request newRequest = Request.create(request.httpMethod(), reconstructedUrl,
			request.headers(), request.body(), request.charset(),
			//這個RequestTemplate可以拿到微服務名稱
			request.requestTemplate());
	return delegate.execute(newRequest, options);
}

所以,我們可以通過繼承FeignBlockingLoadBalancerClient替換默認的實現,來代理調用請求。但是因爲sleuth的存在以及其中的小bug導致RequestTemplate丟失,讓我們拿不到微服務名稱,這個可以參考我提的 PR:replace method for deprecation and keep reference of requestTemplate.但是在Hoxton版本不會合並了,所以需要我們建立同名同路徑類進行替換:org.springframework.cloud.sleuth.instrument.web.client.feign.TracingFeignClient

Request build() {
    if (headers == null) {
        return delegate;
    }
    String url = delegate.url();
    byte[] body = delegate.body();
    Charset charset = delegate.charset();
    //保留requestTemplate
    return Request.create(delegate.httpMethod(), url, headers, body, charset, delegate.requestTemplate());
}

之後,我們實現帶CircuitBreakerThreadPoolBulkheadFeignBlockingLoadBalancerClient,並優化其中的HttpClient:

@Bean
public HttpClient getHttpClient() {
    // 長連接保持30秒
    PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(5, TimeUnit.MINUTES);
    // 總連接數
    pollingConnectionManager.setMaxTotal(1000);
    // 同路由的併發數
    pollingConnectionManager.setDefaultMaxPerRoute(1000);

    HttpClientBuilder httpClientBuilder = HttpClients.custom();
    httpClientBuilder.setConnectionManager(pollingConnectionManager);
    // 保持長連接配置,需要在頭添加Keep-Alive
    httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
    return httpClientBuilder.build();
}

@Bean
public FeignBlockingLoadBalancerClient feignBlockingLoadBalancerCircuitBreakableClient(HttpClient httpClient, BlockingLoadBalancerClient loadBalancerClient, BulkheadRegistry bulkheadRegistry, ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry, CircuitBreakerRegistry circuitBreakerRegistry, RateLimiterRegistry rateLimiterRegistry, RetryRegistry retryRegistry, Tracer tracer) {
    return new FeignBlockingLoadBalancerClient(new CircuitBreakableClient(
            httpClient,
            bulkheadRegistry,
            threadPoolBulkheadRegistry,
            circuitBreakerRegistry,
            rateLimiterRegistry,
            retryRegistry,
            tracer),
            loadBalancerClient);
}

@Log4j2
public static class CircuitBreakableClient extends feign.httpclient.ApacheHttpClient {
    private final BulkheadRegistry bulkheadRegistry;
    private final ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry;
    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final RateLimiterRegistry rateLimiterRegistry;
    private final RetryRegistry retryRegistry;
    private final Tracer tracer;

    public CircuitBreakableClient(HttpClient httpClient, BulkheadRegistry bulkheadRegistry, ThreadPoolBulkheadRegistry threadPoolBulkheadRegistry, CircuitBreakerRegistry circuitBreakerRegistry, RateLimiterRegistry rateLimiterRegistry, RetryRegistry retryRegistry, Tracer tracer) {
        super(httpClient);
        this.bulkheadRegistry = bulkheadRegistry;
        this.threadPoolBulkheadRegistry = threadPoolBulkheadRegistry;
        this.circuitBreakerRegistry = circuitBreakerRegistry;
        this.rateLimiterRegistry = rateLimiterRegistry;
        this.retryRegistry = retryRegistry;
        this.tracer = tracer;
    }

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        String serviceName = request.requestTemplate().feignTarget().name();
        URL url = new URL(request.url());
        String instanceId = serviceName + ":" + url.getHost() + ":" + url.getPort();

        //每個實例一個resilience4j熔斷記錄器,在實例維度做熔斷,所有這個服務的實例共享這個服務的resilience4j熔斷配置
        ThreadPoolBulkhead threadPoolBulkhead;
        CircuitBreaker circuitBreaker;
        try {
            threadPoolBulkhead = threadPoolBulkheadRegistry.bulkhead(instanceId, serviceName);
        } catch (ConfigurationNotFoundException e) {
            threadPoolBulkhead = threadPoolBulkheadRegistry.bulkhead(instanceId);
        }
        try {
            circuitBreaker = circuitBreakerRegistry.circuitBreaker(instanceId, serviceName);
        } catch (ConfigurationNotFoundException e) {
            circuitBreaker = circuitBreakerRegistry.circuitBreaker(instanceId);
        }
        //保持traceId
        Span span = tracer.currentSpan();
        Supplier<CompletionStage<Response>> completionStageSupplier = ThreadPoolBulkhead.decorateSupplier(threadPoolBulkhead,
                CircuitBreaker.decorateSupplier(circuitBreaker, () -> {
                    try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) {
                        log.info("call url: {} -> {}", request.httpMethod(), request.url());
                        Response execute = super.execute(request, options);
                        if (execute.status() != HttpStatus.OK.value()) {
                            throw new ResponseWrapperException(execute.toString(), execute);
                        }
                        return execute;
                    } catch (Exception e) {
                        throw new ResponseWrapperException(e.getMessage(), e);
                    }
                })
        );

        try {
            return Try.ofSupplier(completionStageSupplier).get().toCompletableFuture().join();
        } catch (CompletionException e) {
            Throwable cause = e.getCause();
            if (cause instanceof ResponseWrapperException) {
                ResponseWrapperException responseWrapperException = (ResponseWrapperException) cause;
                if (responseWrapperException.getResponse() != null) {
                    return (Response) responseWrapperException.getResponse();
                }
            }
            throw new ResponseWrapperException(cause.getMessage(), cause);
        }
    }
}

這樣,我們就粘合了Open-Feign,加入了需要的基於實例的熔斷和線程隔離

在 Spring Cloud Gateway 中實現基於實例的熔斷

Spring Cloud Gateway 不用做線程隔離,因爲 reactor 框架不是同步框架,某個實例發生阻塞對它影響不至於很大。我們只在其中做實例級別的熔斷,每個微服務都會做這個實例級別的熔斷。所以需要加一個GlobalFilter。這個熔斷需要在負載均衡器選擇實例並重寫調用 url 之後,也就是在 ReactiveLoadBalancerClientFilter 之後。

由於Spring Cloud Gateway 基於 reactor 做了異步,所以我們需要 spring-cloud-starter-circuitbreaker-reactor-resilience4j裏面的CircuitBreakerOperator將斷路器轉換成 reactor 兼容的Publisher。所以加入了依賴:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

我們只爲了用CircuitBreakerOperator做個轉換而已,其他的並沒有用到。

InstanceCircuitBreakerFilter

/**
 * 實例級別的斷路器
 */
@Log4j2
@Component
public class InstanceCircuitBreakerFilter implements GlobalFilter, Ordered {

    private final CircuitBreakerRegistry circuitBreakerRegistry;
    ObjectMapper objectMapper = new ObjectMapper();


    public InstanceCircuitBreakerFilter(CircuitBreakerRegistry circuitBreakerRegistry) {
        this.circuitBreakerRegistry = circuitBreakerRegistry;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        ServerHttpRequest request = exchange.getRequest();
        String serviceName = request.getHeaders().getFirst(CommonConstant.SERVICE_NAME);
        String instanceId = url.getHost() + url.getPort();
        CircuitBreaker circuitBreaker;
        try {
            //使用實例id新建或者獲取現有的CircuitBreaker,使用serviceName獲取配置
            circuitBreaker = circuitBreakerRegistry.circuitBreaker(instanceId, serviceName);
        } catch (ConfigurationNotFoundException e) {
            circuitBreaker = circuitBreakerRegistry.circuitBreaker(instanceId);
        }

        try {
            log.info("try to send request to: {}: stats: {}", url, objectMapper.writeValueAsString(circuitBreaker.getMetrics()));
        } catch (JsonProcessingException e) {
        }

        return chain.filter(exchange).transform(CircuitBreakerOperator.of(circuitBreaker));
    }

    @Override
    public int getOrder() {
        try {
            //必須在負載均衡器選擇實例並重寫調用 url 之後,也就是在 `ReactiveLoadBalancerClientFilter` 之後。
            return (Integer) ReactiveLoadBalancerClientFilter.class.getDeclaredField("LOAD_BALANCER_CLIENT_FILTER_ORDER").get(null) + 1;
        } catch (Exception e) {
            return 10151;
        }
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章