天天看點

[Spring Cloud] Spring Cloud Hystrix

Spring Cloud Hystrix

目錄

  • Spring Cloud Hystrix
  • Hystrix特性
  • 斷路器機制
  • Fallback
  • 資源隔離
  • 服務熔斷
  • 服務降級
  • 測試
  • REFERENCES
  • 更多
手機使用者請​

​橫屏​

​​擷取最佳閱讀體驗,​

​REFERENCES​

​中是本文參考的連結,如需要連結和更多資源,可以關注其他部落格釋出位址。
平台 位址
簡書 https://www.jianshu.com/u/3032cc862300
個人部落格 https://yiyuery.github.io/NoteBooks/
前言

​雪崩效應​

​,在微服務架構中通常會有多個服務層調用,基礎服務的故障可能會導緻級聯故障,進而造成整個系統不可用的情況,這種現象被稱為服務雪崩效應。服務雪崩效應是一種因“服務提供者”的不可用導緻“服務消費者”的不可用,并将不可用逐漸放大的過程。例如,A作為服務提供者,B為A的服務消費者,C和D是B的服務消費者。A不可用引起了B的不可用,并将不可用像滾雪球一樣放大到C和D時,雪崩效應就形成了。

​Hystrix​

​​是一個用于處理分布式系統的延遲和容錯的開源庫,在分布式系統裡,許多依賴不可避免的會調用失敗,比如逾時、異常等。​

​Hystrix​

​能夠保證在一個依賴出問題的情況下,不會導緻整體服務失敗,避免級聯故障,以提高分布式系統的彈性。

​熔斷器​

​原理很簡單,如同電力過載保護器。它可以實作快速失敗,如果它在一段時間内偵測到許多類似的錯誤,會強迫其以後的多個調用快速失敗,不再通路遠端伺服器,進而防止應用程式不斷地嘗試執行可能會失敗的操作,使得應用程式繼續執行而不用等待修正錯誤,或者浪費CPU時間去等到長時間的逾時産生。熔斷器也可以使應用程式能夠診斷錯誤是否已經修正,如果已經修正,應用程式會再次嘗試調用操作。

​熔斷器模式​

​就像是那些容易導緻錯誤的操作的一種代理。這種代理能夠記錄最近調用發生錯誤的次數,然後決定使用允許操作繼續,或者立即傳回錯誤。熔斷器就是保護服務高可用的最後一道防線。

  • 服務熔斷:熔斷機制是應對雪崩效應的一種微服務鍊路保護機制。
  • 服務降級:在由于非業務異常導緻的服務不可用時,傳回預設值,避免異常影響主業務的處理

Hystrix特性

斷路器機制

斷路器很好了解, 當Hystrix Command請求後端服務失敗數量超過一定比例(預設50%), 斷路器會切換到開路狀态(Open). 這時所有請求會直接失敗而不會發送到後端服務. 斷路器保持在開路狀态一段時間後(預設5秒), 自動切換到半開路狀态(HALF-OPEN). 這時會判斷下一次請求的傳回情況, 如果請求成功, 斷路器切回閉路狀态(CLOSED), 否則重新切換到開路狀态(OPEN). Hystrix的斷路器就像我們家庭電路中的保險絲, 一旦後端服務不可用, 斷路器會直接切斷請求鍊, 避免發送大量無效請求影響系統吞吐量, 并且斷路器有自我檢測并恢複的能力.

Fallback

Fallback相當于是降級操作. 對于查詢操作, 我們可以實作一個fallback方法, 當請求後端服務出現異常的時候, 可以使用fallback方法傳回的值. fallback方法的傳回值一般是設定的預設值或者來自緩存.

資源隔離

在Hystrix中, 主要通過線程池來實作資源隔離. 通常在使用的時候我們會根據調用的遠端服務劃分出多個線程池. 例如調用産品服務的Command放入A線程池, 調用賬戶服務的Command放入B線程池. 這樣做的主要優點是運作環境被隔離開了. 這樣就算調用服務的代碼存在bug或者由于其他原因導緻自己所線上程池被耗盡時, 不會對系統的其他服務造成影響. 但是帶來的代價就是維護多個線程池會對系統帶來額外的性能開銷. 如果是對性能有嚴格要求而且确信自己調用服務的用戶端代碼不會出問題的話, 可以使用Hystrix的信号模式(Semaphores)來隔離資源.

現在讓我們做一些更有趣的事情。由于網關背後的服務可能會對我們的客戶産生不良影響,是以我們可能希望将建立的路由封裝在熔斷器中。您可以在Spring Cloud Gateway中使用Hystrix實作這一點。這是通過一個簡單的過濾器實作的,您可以将其添加到您的請求中。讓我們建立另一個路徑來示範這一點。

服務熔斷

在下面的例子中,我們将利用HTTPBin的延遲API,它在發送響應之前等待一定數量的秒。因為這個API可能需要很長時間來發送它的響應,是以我們可以在HystrixCommand中包裝使用這個API的路由。向RouteLocator對象添加一個新路由,如下所示:

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config.setName("mycmd")))
            .uri("http://httpbin.org:80")).
        build();
}      

這個新路由配置與我們之前建立的路由配置有一些不同。首先,我們使用的是主機位址的正則比對而不是路徑的正則比對。這意味着隻要主機是​

​hystrix.com​

​​,我們就會将請求路由到HTTPBin,并将該請求包裝在​

​HystrixCommand​

​​中。我們通過應用過濾器到路由上來實作這一點。Hystrix過濾器可以使用配置對象來實作配置。在本例中,我們隻是将​

​HystrixCommand​

​​命名為​

​mycmd​

​。

讓我們測試一下這條新路線。啟動應用程式,但這一次我們将請求​

​/get​

​​和​

​/delay/3​

​​。同樣重要的是,我們要包含一個​

​Host​

​​的Header,它擁有​

​hystrix.com​

​​的主機,否則請求将不會被路由轉發。使用​

​curl​

​測試是這樣的:

curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/get
curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/delay/3      
比較幾種請求傳回
  • ​curl http://localhost:8080/get​

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Forwarded": "proto=http;host=\"localhost:8080\";for=\"0:0:0:0:0:0:0:1:62044\"", 
    "Hello": "World", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0", 
    "X-Forwarded-Host": "localhost:8080"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 115.198.1.0, ::1", 
  "url": "https://localhost:8080/get"
}      
  • ​curl --dump-header - --header 'Host:www.hystrix.com' http://localhost:8080/get​

HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: *
Content-Type: application/json
Date: Thu, 28 Mar 2019 13:58:24 GMT
Server: nginx
Content-Length: 361

{
  "args": {}, 
  "headers": {
    "Accept": "*/*", 
    "Forwarded": "proto=http;host=www.hystrix.com;for=\"0:0:0:0:0:0:0:1:62096\"", 
    "Hello": "World", 
    "Host": "httpbin.org", 
    "User-Agent": "curl/7.54.0", 
    "X-Forwarded-Host": "www.hystrix.com"
  }, 
  "origin": "0:0:0:0:0:0:0:1, 115.198.1.0, ::1", 
  "url": "https://www.hystrix.com/get"
}      

當我們請求​

​delay/3​

​時,

curl --dump-header - --header 'Host: www.hystrix.com' http://localhost:8080/delay/3      
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json;charset=UTF-8
Content-Length: 158

{"timestamp":"2019-03-28T14:08:34.337+0000","path":"/delay/3","status":504,"error":"Gateway Timeout","message":"Response took longer than configured timeout"}      
[Spring Cloud] Spring Cloud Hystrix

服務降級

@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .route(p -> p
            .host("*.hystrix.com")
            .filters(f -> f.hystrix(config -> config
                .setName("mycmd")
                .setFallbackUri("forward:/fallback")))
            .uri("http://httpbin.org:80"))
        .build();
}      
定義處理器
/*
 * @ProjectName: 程式設計學習
 * @Copyright:   2019 HangZhou xiazhaoyang Dev, Ltd. All Right Reserved.
 * @address:     http://xiazhaoyang.tech
 * @date:        2019/3/28 23:00
 * @email:       [email protected]
 * @description: 本内容僅限于程式設計技術學習使用,轉發請注明出處.
 */
package com.example.gateway.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *
 * </p>
 *
 * @author xiazhaoyang
 * @version v1.0.0
 * @date 2019/3/28 23:00
 * @modificationHistory=========================邏輯或功能性重大變更記錄
 * @modify By: {修改人} 2019/3/28
 * @modify reason: {方法名}:{原因}
 * ...
 */
@RestController
public class FallBackController {

    @RequestMapping(value = "/fallback")
    public String errorFallBack(){
        return "伺服器開小差了,請稍等...";
    }
}      
[Spring Cloud] Spring Cloud Hystrix

測試

package com.example.gateway;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;

import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions.assertThat;

/**
 * Our test is actually taking advantage of WireMock from Spring Cloud Contract in order stand up a server that can mock the APIs from HTTPBin.
 * The first thing to notice is the use of @AutoConfigureWireMock(port = 0). This annotation will start WireMock on a random port for us.
 * 我們的測試實際上利用了Spring Cloud Contract中的WireMock,以便建立一個可以模拟HTTPBin中的api的伺服器。
 * Next notice that we are taking advantage of our UriConfiguration class and setting the httpbin property in the @SpringBootTest annotation
 * to the WireMock server running locally. Within the test we then setup "stubs" for the HTTPBin APIs we call via the Gateway and
 * mock the behavior we expect. Finally we use WebTestClient to actually make requests to the Gateway and validate the responses.
 *
 * @author xiazhayang
 * @return a
 * @date 2019/3/28 23:25
 * @modify by: {修改人} 2019/3/28 23:25
 * @modify by reason:
 * @since 1.0.0
 */
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {"httpbin=http://localhost:${wiremock.server.port}"})
@AutoConfigureWireMock(port = 0)//使用随機端口
public class CapsuleGatewayApplicationTests {

  @Autowired
  private WebTestClient webClient;

  @Test
  public void contextLoads(){
    //使用WireMock僞造請求 /get 的傳回
    stubFor(get(urlEqualTo("/get"))
        .willReturn(aResponse()
            .withBody("{\"headers\":{\"Hello\":\"World2\"}}")
            .withHeader("Content-Type", "application/json")));
    //僞造請求 /delay/3 的傳回
    stubFor(get(urlEqualTo("/delay/3"))
        .willReturn(aResponse()
            .withBody("fallback msg")
            .withFixedDelay(3000)));

    //get & /delay/3 發起請求
        //并對請求結果進行斷言,若不考慮斷言,會傳回上面mock的資料
    webClient
        .get().uri("/get")
        .exchange()
        .expectStatus().isOk()
        .expectBody()
                //下面是斷言判斷,如果失敗則抛出異常
        .jsonPath("$.headers.Hello").isEqualTo("World2");

    webClient
        .get().uri("/delay/3")
        .header("Host", "www.hystrix.com")
        .exchange()
        .expectStatus().isOk()
        .expectBody();
                //下面是斷言判斷,如果失敗則抛出異常
        //.consumeWith(
            //response -> assertThat(response.getResponseBody()).isEqualTo("伺服器開小差了,請稍等...".getBytes()));
  }
}      

REFERENCES

  • springcloud(四):熔斷器Hystrix
  • Spring Cloud Hystrix

更多

掃碼關注“架構探險之道”,擷取更多源碼和文章資源