在微服務中,經常會出現一些故障,而一些故障會直接或者間接的拖垮其它的服務,造成服務器雪崩,系統就會死掉。
什麼是服務雪崩?我們可以通過下面一張圖來看:
假如現在有很多的用戶同時請求訂單微服務去執行下單的操作,那麼會調用我們的支付微服務,如果支付微服務現在掛掉了,而訂單調用一直沒有響應,由於很多的用戶執行相同的操作,屬於高併發,那麼服務器上積累的訂單越來越多,那麼原來沒有問題的訂單微服務,也會被拖垮,這就是服務雪崩。
我們需要做的就是,當某一個微服務發生蔓延當時候,不能發生故障蔓延,整個系統還能以其它某種方式正常運行,這個就是我們需要解決的。
斷路器
我們耳熟能詳的就是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 提供了一系列增強微服務的可用性功能:
- 斷路器
- 限流
- 基於信號量的隔離
- 緩存
- 限時
- 請求重啓
那麼我們接下來就講解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,可以配置如下參數:
具體的定義如下:
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 項目中寫的,這裏並未涉及到微服務,下篇文章我將和大家分享,這六個功能如何在微服務中使用,進而實現微服務系統的高可用。