Categories
程式開發

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持


本文要點

  • Spring Cloud Hystrix 項目已棄用,因此,新應用程序不應該再使用該項目了。
  • 對於 Spring 開發人員來說, Resilience4j 是實現斷路器模式的一個新選擇。
  • 除了斷路器模式之外, Resilience4j 還提供了限流器、重試、艙壁隔離等特性。
  • Resilience4j 可以很好地與 Spring Boot 配合使用,並且還可以使用 micrometer 庫來發送監控度量指標。
  • 由於 Spring 沒有為 Hystrix Dashboard 引入替代品,所以用戶需要使用Prometheus 或 NewRelic 來搭建監控。

Spring Cloud Hystrix 項目是由 Netflix Hystrix 庫包裝而成的。自從那時起,許多企業和開發人員就開始採用它來實現斷路器(CircuitBreaker )模式了。

2018年11月,Netflix 宣布將這個項目置於維護模式,它也促使 Spring Cloud 宣布了同樣的消息。從那時起,就再沒有對這個 Netflix 庫進行進一步的增強了。在2019年的 SpringOne 上,Spring 宣布將從 Spring Cloud 3.1 版本中移除 Hystrix Dashboard,這促成了它的官方死亡

由於已經大肆宣傳過斷路器模式了,許多開發人員不是已經使用它了,就是正想使用它,因此現在需要一個替代品。Resilience4j 的引入填補這一空白,並且它也為 Hystrix 用戶提供遷移路徑。

Resilience4j

Resilience4j 的靈感來自 Netflix Hystrix,但它是專為 Java 8 和函數式編程而設計的。與Hystrix 相比,它是輕量級的,因為它僅需依賴 Vavr 庫。相比之下,Netflix Hystrix 依賴於Archaius,而 Archaius 還需依賴其他幾個外部庫,比如 Guava 和 ApacheCommons。

與老庫相比,新庫始終有一個優勢,即它可以從以前的錯誤中吸取教訓。 Resilience4j 還提供了許多新特性:

斷路器(CircuitBreaker)

當一個服務調用另一個服務時,另一個服務總有可能停機或具有高延遲。由於服務可能正在等待其他請求完成,所以這可能會導致線程耗盡。斷路器模式的功能與電路的熔斷器類似:

  • 當多個連續失敗超過規定閾值時,斷路器跳閘。
  • 在超時期間,所有調用遠程服務的請求都將立即失敗。
  • 超時到期後,斷路器允許有限數量的測試請求通過。
  • 如果測試請求成功,斷路器將恢復正常工作狀態。
  • 否則,如果還是失敗,超時時間將再次開啟。

限流器(RateLimiter)

限流模式可以確保服務在窗口期間只能接收設定的最大請求數。這樣重點資源可以限量使用,並且能保證其不會被耗盡。

重試(Retry)

重試模式可以使應用程序在調用外部服務時處理瞬態失敗。它可以確保對外部資源進行特定次數的重試操作。如果所有重試都嘗試之後仍沒有成功,那麼它才應失敗,並且應用程序應該能優雅地處理響應。

艙壁隔離(Bulkhead )

艙壁隔離可以確保發生在系統中某一部分的故障不會導致整個系統癱瘓。它能控制並發調用組件的數量。這樣,等待來自該組件的響應資源的數量將會受到限制。艙壁隔離的實現方式有兩種:

  • 信號量隔離方式限制了對服務的並發請求數。一旦達到限制,它會立即拒絕請求。
  • 線程池隔離方式使用線程池將服務與調用方分離,並將其包含到系統資源的子集中。

線程池方式還提供了一個等待隊列,僅當線程池和隊列都滿時才會拒絕請求。管理線程池增加了一些開銷,且與信號量方式相比,它會稍微降低性能,但它允許掛起的線程超時。

使用 Resilience4j 構建 Spring Boot 應用程序

在本文中,我們將構建2個服務:“圖書管理”和“圖書館管理”。

在這個系統中,“圖書館管理”調用“圖書管理”。我們需要操作“圖書管理”服務的上線和下線,以模擬斷路器、限流、重試和艙壁隔離等特性的不同場景。

先決條件

  • JDK 8
  • Spring Boot 2.1.x
  • resilience4j 1.1.x ( resilience4j 最新版本是 1.3,但是 resilience4j-spring-boot2 僅有1.1.x 的最新版本 )
  • 諸如 Eclipse、VSC、 intelliJ 之類的 IDE(最好使用 VSC,因為它非常輕量,與 Eclipse 和intelliJ 相比,我更喜歡它)
  • Gradle
  • NewRelic APM 工具(也可以使用帶有 Grafana 的 Prometheus)

“圖書管理”服務

  1. Gradle 依賴

該服務是一個簡單的基於 REST 的 API,並且需要依賴一些 Web 和測試相關的標準 spring-boot starter jar 包。我們還會使用 Swagger 來測試該 API:

dependencies {
    //REST
    implementation 'org.springframework.boot:spring-boot-starter-web'
    //swagger
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  1. 配置

只需詳細配置一個端口即可:

server:
    port: 8083
  1. 服務實現

服務中包含兩個方法 addBook 和 retrieveBookList 。由於它僅是一個演示示例,所以我們使用了一個 ArrayList 對象來存儲圖書信息:

@Service
public class BookServiceImpl implements BookService {

    List bookList = new ArrayList();

    @Override
    public String addBook(Book book) {
        String message  =   "";
        boolean status  =   bookList.add(book);
        if(status){
            message=    "Book is added successfully to the library.";
        }
        else{
             message=    "Book could not be added in library due to some technical issue. Please try later!";
        }
        return message;
    }

    @Override
    public List retrieveBookList() {
        return bookList;
    }
}
  1. 控制器

Rest Controller 發布了兩個 API,一個是用於添加圖書的 POST API,另一個是用於檢索圖書詳細信息的 GET API:

@RestController
@RequestMapping("/books")
public class BookController {

    @Autowired
    private BookService bookService  ;

    @PostMapping
    public String addBook(@RequestBody Book book){
        return bookService.addBook(book);
    }

    @GetMapping
    public List retrieveBookList(){
        return bookService.retrieveBookList();
    }
}
  1. 測試“圖書管理”服務

通過如下命令構建並啟動應用程序:

//构建应用成功
gradlew build

//启动应用成功
java -jar build/libs/bookmanangement-0.0.1-SNAPSHOT.jar

//端点的 url 链接
http://localhost:8083/books

現在我們可以使用 Swagger UI (http://localhost:8083/swagger-ui.html)來測試該應用程序了。

在開始構建“圖書館管理”服務之前,請先確保該服務已經啟動並處於運行狀態。

“圖書館管理”服務

在這個服務中,我們將使用 Resilience4j 的全部特性。

  1. Gradle依賴

該服務也是一個簡單的基於 REST 的 API,並且也需要依賴一些 Web 和測試相關的 spring-boot starter jar 包。為了在該 API 中使用斷路器和其他 Resilience4j 特性,我們還依賴了一些其他包,比如 resilience4j-spring-boot2、spring-boot starter-actuato r、spring-bootstarter-aop。此外,我們還添加了 micrometer 相關的依賴(micrometer-registry-prometheus,micrometer-registry-new-relic)來啟用監控度量。最後,我們使用了 Swagger 來測試該 API:

dependencies {
    
    compile 'org.springframework.boot:spring-boot-starter-web'
    
    //resilience
    compile "io.github.resilience4j:resilience4j-spring-boot2:${resilience4jVersion}"
    compile 'org.springframework.boot:spring-boot-starter-actuator'
    compile('org.springframework.boot:spring-boot-starter-aop')
 
    //swagger
    compile group: 'io.springfox', name: 'springfox-swagger2', version: '2.9.2'
    implementation group: 'io.springfox', name: 'springfox-swagger-ui', version: '2.9.2'

    // monitoring
        compile "io.micrometer:micrometer-registry-prometheus:${resilience4jVersion}"
      compile 'io.micrometer:micrometer-registry-new-relic:latest.release'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
  1. 配置

此處,我們需要設置的一些配置項。

默認情況下,在 Spring 2.1.x 中,斷路器和限流的執行器 API 是禁用的。我們需要使用管理屬性來啟用它們。在本文末給出的源碼鏈接中可以查看這些屬性。此外,我們還需要配置如下屬性:

  • 配置 NewRelic 觀察 API 的密鑰和賬戶ID
management:
   metrics:
    export:
      newrelic:
        api-key: xxxxxxxxxxxxxxxxxxxxx
        account-id: xxxxx
        step: 1m
  • 為 “add”和“get”服務 API 配置 Resilience4j 斷路器屬性。
 resilience4j.circuitbreaker:
  instances:
    add:
      registerHealthIndicator: true
      ringBufferSizeInClosedState: 5
      ringBufferSizeInHalfOpenState: 3
      waitDurationInOpenState: 10s
      failureRateThreshold: 50
      recordExceptions:
        - org.springframework.web.client.HttpServerErrorException
        - java.io.IOException
        - java.util.concurrent.TimeoutException
        - org.springframework.web.client.ResourceAccessException
        - org.springframework.web.client.HttpClientErrorException
      ignoreExceptions:
  • 為“add”服務 API 配置 Resilience4j 限流器屬性。
resilience4j.ratelimiter:
  instances:
    add:
      limitForPeriod: 5
      limitRefreshPeriod: 100000
          timeoutDuration: 1000ms
  • 為“get”服務 API 配置 Resilience4j 重試屬性。
resilience4j.retry:
  instances:
    get:
      maxRetryAttempts: 3
      waitDuration: 5000
  • 為“get”服務 API 配置 Resilience4j 艙壁隔離屬性。
resilience4j.bulkhead:
  instances:
    get:
      maxConcurrentCall: 10
      maxWaitDuration: 10ms

現在,我們將創建一個 LibraryConfig 類來為 RestTemplate 定義一個bean,以調用“圖書管理”服務。我們還在此處對“圖書管理”服務的端點 URL 進行了硬編碼。對於要上線運行的應用程序來說,這不是一個好主意,但是此演示示例的目的只是為了展示 Resilience4j 的特性。對於線上的應用程序,我們可能要使用服務發現(service-discovery)服務。

@Configuration
public class LibraryConfig {
    Logger logger = LoggerFactory.getLogger(LibrarymanagementServiceImpl.class);
    private static final String baseUrl = "https://bookmanagement-service.apps.np.sdppcf.com";


    @Bean
    RestTemplate restTemplate(RestTemplateBuilder builder) {
        UriTemplateHandler uriTemplateHandler = new RootUriTemplateHandler(baseUrl);
        return builder
                .uriTemplateHandler(uriTemplateHandler)
                .build();
   }
 
}
  1. 服務

服務實現包含一些方法,這些方法使用@CircuitBreaker 、@RateLimiter、@Retry 和@Bulkhead 註解封裝,所有這些註釋都支持fallbackMethod 屬性,並且每個模式在觀察到失敗時,都會將調用重定向到對應的回退(fallback)方法。我們需要定義這些回退方法的實現:

下面這個方法通過 @CircuitBreaker 註解啟用了斷路器功能。因此,如果 /books 端點無法返迴響應,它將會調用 fallbackForaddBook() 方法。

  @Override
    @CircuitBreaker(name = "add", fallbackMethod = "fallbackForaddBook")
    public String addBook(Book book){
        logger.error("Inside addbook call book service. ");
        String response = restTemplate.postForObject("/books", book, String.class);
        return response;
    }

下面這個方法通過 @RateLimiter 註解啟用了限流功能。如果 /books 端點達到上面配置中定義的閾值,它將調用 fallbackForRatelimitBook() 方法。

    @Override
    @RateLimiter(name = "add", fallbackMethod = "fallbackForRatelimitBook")
    public String addBookwithRateLimit(Book book){
        String response = restTemplate.postForObject("/books", book, String.class);
        logger.error("Inside addbook, cause ");
        return response;
    }

下面這個方法通過 @Retry 註解啟用了重試功能。如果 /books端點達到上面配置中定義的閾值,它也將調用 fallbackRetry() 方法。

  @Override
    @Retry(name = "get", fallbackMethod = "fallbackRetry")
    public List getBookList(){
        return restTemplate.getForObject("/books", List.class);
    }

下面這個方法通過 @Bulkhead 註釋啟用了隔離功能。如果 /books 端點達到上面配置中定義的閾值,它也將調用 fallbackBulkhead() 方法。

    @Override
    @Bulkhead(name = "get", type = Bulkhead.Type.SEMAPHORE, fallbackMethod = "fallbackBulkhead")
    public List getBookListBulkhead() {
        logger.error("Inside getBookList bulk head");
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return restTemplate.getForObject("/books", List.class);
    }

搭建完服務層之後,我們需要公開每個方法所對應的 REST API,以便我們對其進行測試。為此,我們需要創建 RestController 類。

  1. 控制器

Rest Controller 公開了4個API:

  • 第一個是一個 POST API,用於添加一本圖書
  • 第二個也是一個POST API,但它用於在限流情況下添加圖書
  • 第三個是一個 GET API,用於檢索圖書的詳細信息
  • 第四個也是一個 GET API,用於在艙壁隔離情況下檢索圖書的詳細信息
@RestController
@RequestMapping("/library")
public class LibrarymanagementController {

    @Autowired
    private LibrarymanagementService librarymanagementService;
    @PostMapping
    public String addBook(@RequestBody Book book){
        return librarymanagementService.addBook(book);
    }

    @PostMapping ("/ratelimit")
    public String addBookwithRateLimit(@RequestBody Book book){
        return librarymanagementService.addBookwithRateLimit(book);
    }

    @GetMapping
    public List getSellersList() {
        return librarymanagementService.getBookList();
    }
    @GetMapping ("/bulkhead")
    public List getSellersListBulkhead() {
        return librarymanagementService.getBookListBulkhead();
    }
}

現在,代碼已經準備好了。但我們必須構建並啟動它。

  1. 構建並測試“圖書館管理”服務

通過如下命令構建並啟動應用程序:

//构建
gradlew build

//启动应用程序
java -jar build/libs/librarymanangement-0.0.1-SNAPSHOT.jar

//端点Url
http://localhost:8084/library

現在我們可以使用 Swagger UI(http://localhost:8084/swagger-ui.html) 來測試該應用程序了。

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 1

圖1

運行斷路器、限流器、重試、艙壁隔離等測試場景

斷路器:斷路器已被應用於 addBook API。為了測試它是否有效,我們將停止“圖書管理”服務。

  • 首先,通過訪問 http://localhost:8084/actuator/health URL來觀察應用程序的運行狀況。
  • 現在停止“圖書管理”服務,並使用 Swagger UI 點擊“圖書館管理”服務的 addBook API

在第一步時,Prometheus 量度應該顯示斷路器的狀態為“CLOSED”。我們是通過 micrometer依賴來啟用 Prometheus 量度的。

執行第二步後,調用將開始失敗並重定向到對應回退方法。

一旦超過了閾值(在本例中為5),它將使觸發斷路。並且,之後的每個調用都將直接定向到回退方法,而不會嘗試使用“圖書管理”服務。 (您可以到日誌文件下觀察日誌輸出語句來驗證這一點。現在,我們觀察 /health 端點,會發現斷路器的狀態為“OPEN”。

{
    "status": "DOWN",
    "details": {
        "circuitBreakers": {
            "status": "DOWN",
            "details": {
                "add": {
                    "status": "DOWN",
                    "details": {
                        "failureRate": "100.0%",
                        "failureRateThreshold": "50.0%",
                        "slowCallRate": "-1.0%",
                        "slowCallRateThreshold": "100.0%",
                        "bufferedCalls": 5,
                        "slowCalls": 0,
                        "slowFailedCalls": 0,
                        "failedCalls": 5,
                        "notPermittedCalls": 0,
                        "state": "OPEN"
                    }                        
                }
            }
        }
    }
}

我們已經在 PCF(Pivotal Cloud Foundry)上部署了相同的代碼,這樣我們就可以將它與NewRelic 集成來創建度量儀錶盤。為此,我們使用了 micrometer-registry-new-relic 依賴。

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 2

圖2 NewRelic 觀察到的斷路器關閉圖

限流器:我們創建了一個單獨的API(http://localhost:8084/library/ratelimit),它具有和addBook 相同的功能,但啟用了限流功能。在這種情況下,我們需要啟動並運行“圖書管理”服務。使用如下限流的配置,每10秒最多可以有5個請求。

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 3

圖3 限流配置

一旦我們在10秒鐘內命中了API 5次 ,它就會達到閾值並被限流。為了避免限流,它將調用回退方法,並根據回退方法的實現邏輯作出響應。下圖顯示,在過去一個小時內,它已經三次達到閾值限流:

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 4

圖4 NewRelic 觀察到的限流器限流

重試:重試功能可以使 API 不斷地重試失敗事務,直到配置的最大次數。如果成功,則將計數刷新成零。如果達到閾值,它將重定向到對應的回退方法並執行相應地邏輯。為了模擬這種情況,我們需要在“圖書管理”服務關閉時點擊 GET API(http://localhost:8084/library)。觀察日誌會發現它正在打印來自回退方法的響應信息。

艙壁隔離:在這個例子中,我們是採用信號量的實現方式來實現艙壁隔離功能的。為了模擬並發調用,我們使用了Jmeter 並在“線程”組中設置了30個用戶調用。

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 5

圖5 Jmeter 配置

我們將點擊一個啟用了艙壁隔離功能的 GET API (),該 API 使用 @Bulkhead 註解,並且我們在其中設置了一段睡眠時間 ,以便它可以達到並發執行的極限。我們觀察日誌會發現,對於某些線程調用,它將轉調對應的回退方法。 API 的可用並發調用如下圖所示:

Spring Cloud Hystrix項目展望:為雲原生 Java 項目提供持續支持 6

圖6 艙壁隔離的可用並發調用指示盤

總結

在本文中,我們學習了一些現在微服務架構必備的特性,這些特性可以使用單個庫 Resilience4j實現。使用帶有 Grafana 或 NewRelic 的 Prometheus,我們可以根據度量指標構建儀錶盤,並能提高系統的穩定性。

與往常一樣,代碼可以通過Github上找到:spring-boot-resilience4j

作者介紹

Rajesh Bhojwani 是一名解決方案架構師,他幫助團隊將應用程序從本地遷移到諸如 PCF 、 AWS 等雲平台。他在應用程序開發、設計和運維方面擁有15年以上的經驗。他是佈道者、技術博主和微服務冠軍。他的最新博客可以在這裡找到。

原文鏈接:

The Future of Spring Cloud’s Hystrix Project