SpringCloud之Resilience4J用法精講

在微服務中,經常會出現一些故障,而一些故障會直接或者間接的拖垮其它的服務,造成服務器雪崩,系統就會死掉。

什麼是服務雪崩?我們可以通過下面一張圖來看:
在這裏插入圖片描述
假如現在有很多的用戶同時請求訂單微服務去執行下單的操作,那麼會調用我們的支付微服務,如果支付微服務現在掛掉了,而訂單調用一直沒有響應,由於很多的用戶執行相同的操作,屬於高併發,那麼服務器上積累的訂單越來越多,那麼原來沒有問題的訂單微服務,也會被拖垮,這就是服務雪崩。

我們需要做的就是,當某一個微服務發生蔓延當時候,不能發生故障蔓延,整個系統還能以其它某種方式正常運行,這個就是我們需要解決的。

斷路器

我們耳熟能詳的就是Netflix Hystrix,這個斷路器是SpringCloud中最早支持的一種容錯方案,現在這個斷路器已經處於維護狀態,已經不再更新了,你仍然可以使用這個斷路器,但是呢,我不建議你去使用,因爲這個已經不再更新,所以Spring官方已經出現了Netflix Hystrix的替換方案。 如下圖:
在這裏插入圖片描述
在 Spring Cloud Greenwich 版中,對於 Hystrix 以及 Hystrix Dashboard 官方都給出了替代方案。我們整個教程雖然基於最新的 Spring Cloud Greenwich 版,但是考慮到現實情況,本文中我還是先向大家大致介紹一下 Hystrix 的功能,後面我們會詳細介紹 Resilience4j 的用法。

服務熔斷

什麼是服務熔斷呢?服務熔斷就是當A服務去調用B服務,如果A服務遲遲沒有收到B服務的響應,那麼就終斷當前的請求,而不是一直等待下去,一直等待下去的結果就是拖垮其它的服務。當系統發生熔斷的時候,我們還要去監控B服務,當B服務恢復正常使用時,A服務就發起重新調用的請求。

服務降級

當我們的服務發生熔斷的時候,那麼就需要降級了,那麼什麼是降級?降級指的是A服務調用B服務,沒有調用成功,發生熔斷,那麼A服務就不要死板的一直請求B服務,而是去服務上哪一個緩存先頂着,避免給我們的用戶,響應一些錯誤的頁面,這個就是服務降級。

請求緩存

請求緩存是指對接口進行緩存,這樣可以大大降低服務提供者的壓力,當然我們要選擇緩存使用的場景,是那種更新頻率低,但是訪問又比較頻繁的數據。

我們這裏說的緩存啊,指的是Hystrix的緩存,但是在實際的開發中,我們可能會配合其它的緩存來實現更好的效果,如redis。

請求合併

我們知道SpringCloud中的微服務之間的調用都是通過HTTP來實現的,但是我們通過HTTP協議調用微服務時候,如果是高併發數量小的話,那麼效率很低,那麼我們可以通過合併請求來實現,也就是將客戶端多個請求合併成一個請求,也就是隻發送一個HTTP請求,服務器拿到請求結果後,再將請求結果分發給不同的請求,這樣就可以提供傳輸效率。

Resilience4J

Resilience4J是我們Spring Cloud G版本 推薦的容錯方案,它是一個輕量級的容錯庫。它呢借鑑了Hystrix而設計,並且採用JDK8 這個函數式編程,也就是我們的lambda表達式,爲什麼說它是輕量級的呢?因爲它的庫只使用 Vavr (以前稱爲 Javaslang ),它沒有任何其他外部庫依賴項。相比之下, Netflix Hystrix 對Archaius 具有編譯依賴性,這導致了更多的外部庫依賴,例如 Guava 和 Apache Commons 。而如果使用Resilience4j,你無需引用全部依賴,可以根據自己需要的功能引用相關的模塊即可。

Resilience4J 提供了一系列增強微服務的可用性功能:

  1. 斷路器
  2. 限流
  3. 基於信號量的隔離
  4. 緩存
  5. 限時
  6. 請求重啓

那麼我們接下來就講解Resilience4J的幾種功能的使用方法,由於是基本用法所以我們不用創建SpringBoot工程,我們只需要創建一個叫Resilience4j的普通maven工程並加入 junit 單元測試依賴,這樣準備工作就完成了。

junit單元測試

  <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
  </dependency>

斷路器初始化

我們使用的是Resilience4J 提供的斷路器功能,那麼就要加入依賴


  <dependency>
         <groupId>io.github.resilience4j</groupId>
         <artifactId>resilience4j-circuitbreaker</artifactId>
         <version>0.13.2</version>
  </dependency>

這個依賴它提供的是一個基於ConcurrentHashMap 的 CircuitBreakerRegistry ,CircuitBreakerRegistry 是線程安全的,並且是原子操作。開發者可以使用 CircuitBreakerRegistry 來創建和檢索 CircuitBreaker 的實例 ,開發者可以直接使用默認的全局CircuitBreakerConfig 爲所有 CircuitBreaker 實例創建 CircuitBreakerRegistry ,如下所示:

CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();

我們使用自定義的CircuitBreakerConfig,可以配置如下參數:

  • 故障率閾值百分比,超過這個閾值,斷路器就會打開
  • 斷路器保持打開的時間,在到達設置的時間之後,斷路器會進入到 half open 狀態
  • 當斷路器處於 half open 狀態時,環形緩衝區的大小
  • 當斷路器關閉時,環形緩衝區的大小
  • 自定義斷路器中的事件操作
  • 自定義 Predicate 以便計算異常是否被記錄爲失敗事件
  • 具體的定義如下:

    CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
        .failureRateThreshold(50)
        .waitDurationInOpenState(Duration.ofMillis(1000))
        .ringBufferSizeInHalfOpenState(2)
        .ringBufferSizeInClosedState(2)
        .build();
    CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.of(circuitBreakerConfig);
    CircuitBreaker circuitBreaker2 = circuitBreakerRegistry.circuitBreaker("otherName");
    CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("uniqueName", circuitBreakerConfig);
    

    上面這段代碼解釋如下:
    我們首先定義了一個CircuitBreakerConfig 對象,在定義CircuitBreakerConfig對象時,設置故障率爲50%,斷路器 保持打開時間爲2秒,斷路器處於half open的時候,緩衝區大小爲2,當對象處於關閉時,緩衝區的大小也是2,然後根據CircuitBreakerConfig對象創建
    CircuitBreakerRegistry,然後再根據CircuitBreakerRegistry 創建兩個斷路器CircuitBreaker。

    如果不想使用CircuitBreakerRegistry來管理斷路器 那麼可以直接創建CircuitBreaker對象

    CircuitBreaker defaultCircuitBreaker = CircuitBreaker.ofDefaults("testName");
    CircuitBreaker customCircuitBreaker = CircuitBreaker.of("testName", circuitBreakerConfig);
    

    斷路器的使用案例

    斷路器使用了裝飾者模式,開發者可以使用 CircuitBreaker.decorateCheckedSupplier(), CircuitBreaker.decorateCheckedRunnable() 或者 CircuitBreaker.decorateCheckedFunction() 來裝飾 Supplier / Runnable / Function 或者 CheckedRunnable / CheckedFunction,然後使用 Try.of(…​) 或者 Try.run(…​) 來進行調用操作,也可以使用 map、flatMap、filter、recover 或者 andThen 進行鏈式調用,但是調用這些方法斷路器必須處於 CLOSED 或者 HALF_OPEN 狀態。例如下面一個例子,創建一個斷路器出來,首先裝飾了一個函數,這個函數返回一段字符串,然後使用 Try.of 去執行,執行完後再進入到 map 中去執行。如果第一個函數正常執行第二個函數纔會執行,如果第一個函數執行失敗,那麼 map 函數將不會執行:

    CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
    CheckedFunction0<String> decoratedSupplier = CircuitBreaker
            .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");
    Try<String> result = Try.of(decoratedSupplier)
            .map(value -> value + " world'");
    System.out.println(result.isSuccess());
    System.out.println(result.get());
    

    你可以將不同的斷路器連接起來:

    CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
    CircuitBreaker anotherCircuitBreaker = CircuitBreaker.ofDefaults("anotherTestName");
    CheckedFunction0<String> decoratedSupplier = CircuitBreaker
            .decorateCheckedSupplier(circuitBreaker, () -> "Hello");
    CheckedFunction1<String, String> decoratedFunction = CircuitBreaker
            .decorateCheckedFunction(anotherCircuitBreaker, (input) -> input + " world");
    Try<String> result = Try.of(decoratedSupplier)
            .mapTry(decoratedFunction::apply);
    System.out.println(result.isSuccess());
    System.out.println(result.get());
    

    斷路器的打開

    這裏創建了兩個 CircuitBreaker ,裝飾了兩個函數,第二次使用了 mapTry 方法來連接。前面給大家演示的幾種情況,都是執行成功的,即斷路器一直處於關閉的狀態,接下來給大家再來演示一個斷路器打開的例子,如下:

    CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
            .ringBufferSizeInClosedState(2)
            .waitDurationInOpenState(Duration.ofMillis(1000))
            .build();
    CircuitBreaker circuitBreaker = CircuitBreaker.of("testName", circuitBreakerConfig);
    circuitBreaker.onError(0, new RuntimeException());
    System.out.println(circuitBreaker.getState());
    circuitBreaker.onError(0, new RuntimeException());
    System.out.println(circuitBreaker.getState());
    Try<String> result = Try.of(CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> "Hello"))
            .map(value -> value + " world");
    System.out.println(result.isSuccess());
    System.out.println(result.get());
    
    

    這裏手動模擬錯誤,首先設置了斷路器關閉狀態下的環形緩衝區大小爲 2 ,即當有兩條數據時就可以去統計故障率了,這裏沒有設置故障率,默認的故障率是 50% ,當第一次調用 onError 方法後,打印斷路器當前狀態,發現斷路器還是處於關閉狀態,並未打開,接下來再次調用 onError 方法,然後再去查看斷路器狀態,此時發現斷路器已經打開了,因爲滿足了 50% 的故障率了。

    斷路器重置

    斷路器的重置就是清空數據

    circuitBreaker.reset();
    

    服務器請求降級

    服務器降級操作如下:

    CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
    CheckedFunction0<String> checkedSupplier = CircuitBreaker.decorateCheckedSupplier(circuitBreaker, () -> {
        throw new RuntimeException("BAM!");
    });
    Try<String> result = Try.of(checkedSupplier)
            .recover(throwable -> "Hello Recovery");
    System.out.println(result.isSuccess());
    System.out.println(result.get());
    

    如果需要使用服務降級,可以使用 Try.recover() 鏈接,當 Try.of() 返回 Failure 時服務降級會被觸發。

    狀態監聽

    狀態監聽可以獲取到熔斷器當前的運行數據,例如:

    CircuitBreaker.Metrics metrics = circuitBreaker.getMetrics();
    // 獲取故障率
    float failureRate = metrics.getFailureRate();
    // 獲取調用失敗次數
    int failedCalls = metrics.getNumberOfFailedCalls();
    

    限流

    RateLimiter 和我們前面提到的斷路器實際上非常類似,它也有一個基於內存的 RateLimiterRegistry 和 RateLimiterConfig 可以配置,我們可以配置如下一些參數:

  • 限流之後的冷卻時間
  • 閾值刷新時間
  • 閾值刷新頻次
  • 使用限流我們要引入下面的依賴:

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-ratelimiter</artifactId>
        <version>0.13.2</version>
    </dependency>
    

    基本用法

    例如,想限制某個請求的頻率爲 2QPS(每秒處理兩個請求),爲什麼給一個這樣的頻率呢?主要是爲了大家一會兒測試方便,代碼如下:

    RateLimiterConfig config = RateLimiterConfig.custom()
            .limitRefreshPeriod(Duration.ofMillis(1000))
            .limitForPeriod(2)
            .timeoutDuration(Duration.ofMillis(1000))
            .build();
    RateLimiterRegistry rateLimiterRegistry = RateLimiterRegistry.of(config);
    RateLimiter rateLimiterWithDefaultConfig = rateLimiterRegistry.rateLimiter("backend");
    RateLimiter rateLimiterWithCustomConfig = rateLimiterRegistry.rateLimiter("backend#2", config);
    RateLimiter rateLimiter = RateLimiter.of("NASDAQ :-)", config);
    

    和前面的一樣,我們也可以使用 RateLimiterRegistry 來統一管理 RateLimiter ,也可以通過 RateLimiter.of 方法來直接創建一個 RateLimiter。創建好了,就可以直接使用了,代碼如下:

    CheckedRunnable restrictedCall = RateLimiter
            .decorateCheckedRunnable(rateLimiter,()->{
                System.out.println(new Date());
            });
    Try.run(restrictedCall)
            .andThenTry(restrictedCall)
            .andThenTry(restrictedCall)
            .andThenTry(restrictedCall)
            .onFailure(throwable -> System.out.println(throwable.getMessage()));
    

    執行結果如下:

    在這裏插入圖片描述
    可以觀察上面,我們發現可以知道限流一次執行了兩個方法,另外兩個方法在1s過後執行的。並且限流參數是可以隨便修改的,修改後,本次的限流週期內不會生效,下次限流纔會生效執行。

    修改限流如下:

    rateLimiter.changeLimitForPeriod(100);
    rateLimiter.changeTimeoutDuration(Duration.ofMillis(100));
    

    事件監聽

    在限流中,我們可以獲取所有允許和拒絕執行的事件信息,獲取方式如下:

    rateLimiter.getEventPublisher()
            .onSuccess(event -> {
                System.out.println(new Date()+">>>"+event.getEventType()+">>>"+event.getCreationTime());
            })
            .onFailure(event -> {
                System.out.println(new Date()+">>>"+event.getEventType()+">>>"+event.getCreationTime());
            });
    

    請求隔離

    這裏的請求隔離,主要是基於信號量的請求隔離,不包含基於線程的請求隔離,具體用法和前面兩個類似,不過在使用之前,需要先添加請求隔離相關的依賴,如下:

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-bulkhead</artifactId>
        <version>0.13.2</version>
    </dependency>
    

    定義最大並行數和飽和狀態Bulkhead時 線程的最大阻塞時間 如下:

      BulkheadConfig config = BulkheadConfig.custom()
                    .maxConcurrentCalls(150)
                    .maxWaitTime(100)
                    .build();
            BulkheadRegistry registry = BulkheadRegistry.of(config);
            Bulkhead bulkhead1 = registry.bulkhead("foo");
            BulkheadConfig custom = BulkheadConfig.custom()
                    .maxWaitTime(0).build();
    
    
            Bulkhead bulkhead2 = registry.bulkhead("bar", custom);
            System.out.println(bulkhead1 + ">>>>>" + bulkhead2);
        }
    

    輸出結果爲:
    在這裏插入圖片描述

    如果不想通過BulkheadRegistry來管理Bulkhead的實例,那麼我們可以直接創建Bulkhead如下:

      Bulkhead bulkhead1 = Bulkhead.ofDefaults("foo");
            Bulkhead bulkhead2 = Bulkhead.of("bar",
                    BulkheadConfig.custom().maxConcurrentCalls(50).build());
            System.out.println(bulkhead1 + ">>>>>" + bulkhead2);
    

    運行效果如下:
    在這裏插入圖片描述
    創建好來後,使用的方法和斷路器是一樣的:

     BulkheadConfig config = BulkheadConfig.custom()
                    .maxConcurrentCalls(1)
                    .maxWaitTime(100)
                    .build();
            Bulkhead bulkhead = Bulkhead.of("testName", config);
            CheckedFunction0<String> decoratedSupplier = Bulkhead.decorateCheckedSupplier(
                    bulkhead,()-> "this is bulkhead: love "
            );
    
            Try<String> result = Try.of(decoratedSupplier).map(
                    value -> value + "小蕾"
            );
    
            System.out.println(result.isSuccess());
            System.out.println(result.get());
        }
    

    執行結果如下:
    在這裏插入圖片描述

    請求重試

    當我們的服務失敗的時候,那麼就需要請求重試,可以說請求重試是一個非常常用的功能,要使用請求 重試的話,那麼要引入下面這個依賴:

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-retry</artifactId>
        <version>0.13.2</version>
    </dependency>
    

    那麼引入依賴後,我們創建一個重試的測試用例:

      RetryConfig config = RetryConfig.custom()
       .maxAttempts(3) //重試次數爲3次
       .waitDuration(Duration.ofMillis(500)) //每次重試間隔500毫秒
       .build();
      Retry retry = Retry.of("id", config);
    

    創建好 Retry的實例後,我們就可以使用了,使用的步驟和斷路器是一樣的如下:

     CheckedFunction0<String> retryAllSupplier = Retry.decorateCheckedSupplier(
                    retry, () -> {
                        System.out.println("date:" + new Date() + ":" + Math.random());
                        return "love 小蕾";
                    });
    
            Try<String> result = Try.of(retryAllSupplier).recover((throwable -> "Hello world from recovery function"));
            System.out.println(result.isSuccess());
            System.out.println(result.get());
    

    運行的結果如下:
    在這裏插入圖片描述

    如果拋出了異常那麼就會觸發重試機制。

    緩存

    Resilience4J 提供了 JCache 緩存,但是我們實際開發用的是Redis緩存,這裏就不多講了,感興趣的朋友,可以自己去學習。

    限時

    Resilience4j 中的限時器是要結合 Future 一起來使用,開發者需要提前配置過期時間,在過期時間內要是沒有獲取到value,那麼 Future 將會被取消,使用步驟如下:
    先引入依賴

    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-timelimiter</artifactId>
        <version>0.13.2</version>
    </dependency>
    

    使用代碼如下:

       TimeLimiterConfig config = TimeLimiterConfig.custom()
                    .timeoutDuration(Duration.ofSeconds(60))
                    .cancelRunningFuture(true)
                    .build();
            TimeLimiter timeLimiter = TimeLimiter.of(config);
            ExecutorService executorService = Executors.newSingleThreadExecutor();
            Supplier<Future<Integer>> futureSupplier = () -> executorService.submit(userService::doSomething);
            Callable restrictedCall = TimeLimiter
                    .decorateFutureSupplier(timeLimiter, futureSupplier);
            Try.of(restrictedCall.call)
                    .onFailure(throwable -> System.out.println(throwable.getMessage()));
    

    這裏首先創建了一個 TimeLimiter,然後將任務放到線程池中,獲取到一個 Supplier 對象,然後使用限時器包裝該對象,當調用超時, onFailure 方法就會被觸發。

    也可以將限時器和斷路器結合使用,當調用超時次數過多,直接熔斷,如下:

    Callable restrictedCall = TimeLimiter
        .decorateFutureSupplier(timeLimiter, futureSupplier);
    Callable chainedCallable = CircuitBreaker.decorateCallable(circuitBreaker, restrictedCall);
    Try.of(chainedCallable::call)
        .onFailure(throwable -> LOG.info("We might have timed out or the circuit breaker has opened."));
    

    總結

    本文首先向大家介紹了傳統的容錯方案 Hystrix 的一些大致功能,這個讀者作爲了解即可;然後向讀者介紹了 Resilience4j 的一些基本功能,這些基本功能涵蓋了請求熔斷、限流、限時、緩存、隔離以及重試,這裏我們只是介紹了 Resilience4j 的一些基本用法。上文中所有的案例都是在一個普通的 JavaSE 項目中寫的,這裏並未涉及到微服務,下篇文章我將和大家分享,這六個功能如何在微服務中使用,進而實現微服務系統的高可用。

    項目地址

    github

    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章