如何不啓用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>
之後,會自動加載BulkheadRegistry
,ThreadPoolBulkheadRegistry
,CircuitBreakerRegistry
,RateLimiterRegistry
,RetryRegistry
這幾個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
這裏主要用到的元素是:CircuitBreaker
和ThreadPoolBulkhead
。CircuitBreaker
用來實現實例級別的熔斷,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
被調用,選取好要發送請求的實例之後,加入CircuitBreaker
和ThreadPoolBulkhead
。也就是,我們需要拿到本次請求調用的實例,以及微服務名稱,加載對應的CircuitBreaker
和ThreadPoolBulkhead
,包裝調用請求,之後執行調用。
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());
}
之後,我們實現帶CircuitBreaker
和ThreadPoolBulkhead
的FeignBlockingLoadBalancerClient
,並優化其中的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
做個轉換而已,其他的並沒有用到。
/**
* 實例級別的斷路器
*/
@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;
}
}
}