Hystrix斷路器在微服務網關中的應用

本文主要是解決Hystrix過濾器應用過程中的報錯問題,並提供正確的使用方式。

問題分析

熔斷機制和日常生活中見到電路保險絲是非常相似的,當出現了問題之後,保險絲會自動燒斷,以保護我們的電器。在我們的對外提供服務時,當現在服務的提供方出現了問題之後整個的程序將出現錯誤的信息顯示,而這個時候如果不想出現這樣的錯誤信息,而希望替換為一個錯誤時的內容。

一個服務掛了後續的服務跟著不能用了,這就是雪崩效應。

我們在網關配置了Hystrix斷路器的過濾器:

 routes:
- id: hytstrix_route
uri: lb://user
order: 6000
predicates:
- Path=/user/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallbackcontroller?a=123

出現錯誤之後可以 fallback 錯誤的處理信息。此外,Hystrix斷路器經常結合 Feign一起使用,還需要在Feign(客戶端)進行熔斷的配置。

依賴版本

spring-boot-starter-parent的版本為2.0.3.RELEASE。 Spring Cloud的版本為Finchley.RELEASE,對應的spring-cloud-gateway版本為2.0.0.RELEASE。

報錯分析

使用POSTMAN發送GET請求,不會出現第一小節的異常。當改為POST請求之後,HystrixGatewayFilterFactory拋出異常。使得剛開始的猜想往為什麼不支持POST請求上考慮。打開debug日誌,我們得到如下更為詳細的輸出:

AbstractCommand$22.call[821] : HystrixCommand execution COMMAND_EXCEPTION and fallback failed.
java.lang.IllegalArgumentException: Actual request host must not be null
at org.springframework.util.Assert.notNull(Assert.java:193)
at org.springframework.web.cors.reactive.CorsUtils.isSameOrigin(CorsUtils.java:74)
at org.springframework.web.cors.reactive.DefaultCorsProcessor.process(DefaultCorsProcessor.java:70)
at org.springframework.web.reactive.handler.AbstractHandlerMapping.lambda$getHandler$1(AbstractHandlerMapping.java:152)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:107)
at reactor.core.publisher.Operators$ScalarSubscription.request(Operators.java:1640)
at

從上面的日誌可以看出,最後的錯誤定位到了默認的CORS處理器DefaultCorsProcessor的實現。

public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
HttpServletResponse response) throws IOException {
//是否為CORS請求(包含Origin頭部)
if (!CorsUtils.isCorsRequest(request)) {
return true; //不是則直接返回
}
ServletServerHttpResponse serverResponse = new ServletServerHttpResponse(response);
//根據serverResponse響應判斷Access-Control-Allow-Origin
if (responseHasCors(serverResponse)) {
logger.debug("Skip CORS processing: response already contains "Access-Control-Allow-Origin" header");
return true;
}
ServletServerHttpRequest serverRequest = new ServletServerHttpRequest(request);
if (WebUtils.isSameOrigin(serverRequest)) {
logger.debug("Skip CORS processing: request is from same origin");
return true;
}
boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);
if (config == null) {

if (preFlightRequest) {
rejectRequest(serverResponse);
return false;
}
else {
return true;
}
}
return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
}

如上是報錯部分的代碼,這段代碼的功能是基於cors的配置,處理給定的請求。首先判斷是否為CORS的請求,是則直接返回true;否則判斷響應中的頭部Access-Control-Allow-Origin是否為空(Access-Control-Allow-Origin是HTML5中定義的一種解決資源跨域的策略。他是通過服務器端返回帶有Access-Control-Allow-Origin標識的Response header,用來解決資源的跨域權限問題,表示接受哪些域名的請求);否則 基於Origin、Host、Forwarded、X-Forwarded-Proto、X-Forwarded-Host、X-Forwarded-Port等頭部,校驗請求是否同源。

對於非簡單請求,CORS機制跨域會首先進行 preflight(一個 OPTIONS 請求), 該請求成功後才會發送真正的請求。 這一設計旨在確保服務器對 CORS 標準知情,以保護不支持CORS的舊服務器。

到這一步,會判斷CORS的配置是否為空,如果為空,且不是一個preflight請求,則返回true,否則返回false;再下一步進入CORS的配置不為空的處理邏輯,此處略過。

這裡我們拓展一下,瀏覽器將CORS請求分為兩類:簡單請求(simple request)和非簡單請求(not-simple-request),簡單請求瀏覽器不會預檢,而非簡單請求會預檢。

這兩種方式怎麼區分?同時滿足下列三大條件,就屬於簡單請求,否則屬於非簡單請求

  1. 請求方式只能是:GET、POST、HEAD
  2. HTTP請求頭限制這幾種字段:Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID
  3. Content-type只能取:application/x-www-form-urlencoded、multipart/form-data、text/plain。

對於簡單請求,瀏覽器直接請求,會在請求頭信息中,增加一個origin字段,來說明本次請求來自哪個源(協議+域名+端口)。服務器根據這個值,來決定是否同意該請求,服務器返回的響應會多幾個頭信息字段,如下所示:

  1. Access-Control-Allow-Origin:該字段是必須的,* 表示接受任意域名的請求,還可以指定域名
  2. Access-Control-Allow-Credentials:該字段可選,是個布爾值,表示是否可以攜帶cookie,(注意:如果Access-Control-Allow-Origin字段設置*,此字段設為true無效)
  3. Access-Control-Allow-Headers:該字段可選,裡面可以獲取Cache-Control、Content-Type、Expires等,如果想要拿到其他字段,就可以在這個字段中指定。

上面的頭信息中,三個與CORS請求相關,都是以Access-Control-開頭。

非簡單請求是對那種對服務器有特殊要求的請求,比如請求方式是PUT或者DELETE,或者Content-Type字段類型是application/json。都會在正式通信之前,增加一次HTTP請求,稱之為預檢。瀏覽器會先詢問服務器,當前網頁所在域名是否在服務器的許可名單之中,服務器允許之後,瀏覽器會發出正式的XMLHttpRequest請求,否則會報錯。

回顧我們的業務場景,來自客戶端的請求,到達網關後將會轉發到具體服務,此時對應的服務是down的狀態,返回的響應結果為空。我們在網關沒有任何的CORS配置,因此按照上述的CORS處理邏輯,返回的結果為false。

當目標服務的狀態是正常的,請求得到相應,CORS處理是正常的;因此,出錯的根源在於,當我們的請求頭中攜帶Origin時,目標服務的不可用將會導致如上的錯誤,這顯然不是我們想要的結果。

解決思路

對於上述問題,圍繞CORS的處理,我們有如下幾種解決思路。

移除請求的頭部Origin

移除請求的頭部Origin,從CORS處理的邏輯得知,當該請求不是一個CORS請求(即不包含頭部Origin),處理的過程就結束,這樣可以避免後續的檢查。

修改配置如下:

 routes:
- id: hytstrix_route
uri: lb://user
order: 6000
predicates:
- Path=/user/**
filters:
- StripPrefix=1
- RemoveRequestHeader=Origin
- name: Hystrix
args:
name: fallbackcmd
fallbackUri: forward:/fallbackcontroller?a=123

再次發送請求,無論是GET還是POST,攜帶頭部Origin都可以正常fallback。

CORS配置

我們還可以增加CORS的過濾器,手動增加響應的頭部信息。

@Bean
public WebFilter corsFilter() {
return (ServerWebExchange ctx, WebFilterChain chain) -> {
ServerHttpRequest request = ctx.getRequest();
if (CorsUtils.isCorsRequest(request)) {
ServerHttpResponse response = ctx.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", ALLOWED_ORIGIN);
headers.add("Access-Control-Allow-Methods", ALLOWED_METHODS);
headers.add("Access-Control-Max-Age", MAX_AGE);
headers.add("Access-Control-Allow-Headers",ALLOWED_HEADERS);
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
}
return chain.filter(ctx);
};
}

Spring Cloud Gateway版本升級

自2.0.1.RELEASE版本開始,Spring Cloud Gateway提供了全局cors的配置:

spring:
cloud:
gateway:
globalcors:
corsConfigurations:
'[/**]':
allowedOrigins: "*"
allowedMethods: "*"

通過如上配置,可以實現與上一小節相同的功能。

小結

本文主要講了Hystrix過濾器在網關中的應用時遇到的問題,通過錯誤信息,debug源碼尋找問題的根源。之後我們分析了問題,並根據問題的根源提出了幾種可行的解決方案。解決問題的方法有多種,本文主要是提供一個排查問題和解決問題的思路。關注、轉發、評論頭條號每天分享java 知識,私信回覆“555”贈送一些Dubbo、Redis、Netty、zookeeper、Spring cloud、分佈式資料


分享到:


相關文章: