Hystrix斷路器在微服務閘道器中的應用

aoho發表於2018-12-14

前文回顧

在之前的一篇文章:微服務閘道器Zuul遷移到Spring Cloud Gateway,我們講解了如何從Zuul遷移到新的元件:Spring Cloud Gateway,以及擴充套件了微服務閘道器的功能,包括限流過濾器、斷路器過濾器等。然而很多讀者在使用的時候反饋,使用POSTMAN傳送GET請求測試斷路器是正常的,然而POST請求會出現:

{
    "timestamp": "2018-10-11T13:07:07.790+0000",
    "path": "/user/body",
    "status": 500,
    "error": "Internal Server Error",
    "message": "fallbackcmd failed and fallback failed."
}
複製程式碼

看一下閘道器服務的控制檯,HystrixGatewayFilterFactory也報錯。

com.netflix.hystrix.exception.HystrixRuntimeException: fallbackcmd failed and fallback failed.
	at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:825) ~[hystrix-core-1.5.12.jar:1.5.12]
	at com.netflix.hystrix.AbstractCommand$22.call(AbstractCommand.java:804) ~[hystrix-core-1.5.12.jar:1.5.12]
	at rx.internal.operators.OperatorOnErrorResumeNextViaFunction$4.onError(OperatorOnErrorResumeNextViaFunction.java:140) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87) ~[rxjava-1.3.8.jar:1.3.8]
	at com.netflix.hystrix.AbstractCommand$DeprecatedOnFallbackHookApplication$1.onError(AbstractCommand.java:1472) ~[hystrix-core-1.5.12.jar:1.5.12]
	at com.netflix.hystrix.AbstractCommand$FallbackHookApplication$1.onError(AbstractCommand.java:1397) ~[hystrix-core-1.5.12.jar:1.5.12]
	at rx.internal.operators.OnSubscribeDoOnEach$DoOnEachSubscriber.onError(OnSubscribeDoOnEach.java:87) ~[rxjava-1.3.8.jar:1.3.8]
	at rx.internal.reactivestreams.SubscriberAdapter.onError(SubscriberAdapter.java:59) ~[rxjava-reactive-streams-1.2.1.jar:1.2.1]

複製程式碼

本文主要是解決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原始碼尋找問題的根源。之後我們分析了問題,並根據問題的根源提出了幾種可行的解決方案。解決問題的方法有多種,本文主要是提供一個排查問題和解決問題的思路。

訂閱最新文章,歡迎關注我的公眾號

微信公眾號

參考

  1. Hystrix filter throw exception with post request
  2. http跨域時的options請求

相關文章