歡迎訪問我的GitHub
https://github.com/zq2599/blog_demos
內容:所有原創文章分類彙總及配套原始碼,涉及Java、Docker、Kubernetes、DevOPS等;
本篇概覽
- 一起深入瞭解Spring Cloud Gateway的斷路器(CircuitBreaker)功能:
- 先聊聊理論
- 再結合官方和大神的資訊確定技術棧
- 再動手開發,先實現再驗證
- 再趁熱打鐵,看看它的原始碼
- 最後,回顧一下有哪些不足(下一篇文章解決這些不足)
關於斷路器(CircuitBreaker)
- 下圖來自resilience4j官方文件,介紹了什麼是斷路器:
- CLOSED狀態時,請求正常放行
- 請求失敗率達到設定閾值時,變為OPEN狀態,此時請求全部不放行
- OPEN狀態持續設定時間後,進入半開狀態(HALE_OPEN),放過部分請求
- 半開狀態下,失敗率低於設定閾值,就進入CLOSE狀態,即全部放行
- 半開狀態下,失敗率高於設定閾值,就進入OPEN狀態,即全部不放行
確認概念
- 有個概念先確認一下,即<font color="blue">Spring Cloud斷路器</font>與<font color="blue">Spring Cloud Gateway斷路器功能</font>不是同一個概念,Spring Cloud Gateway斷路器功能還涉及過濾器,即在過濾器的規則下使用斷路器:
- 本篇的重點是Spring Cloud Gateway如何配置和使用斷路器(CircuitBreaker),因此不會討論Resilience4J的細節,如果您想深入瞭解Resilience4J,推薦資料是Spring Cloud Circuit Breaker
關於Spring Cloud斷路器
- 先看Spring Cloud斷路器,如下圖,Hystrix、Sentinel這些都是熟悉的概念:
關於Spring Cloud Gateway的斷路器功能
- 來看Spring Cloud Gateway的官方文件,如下圖,有幾個關鍵點稍後介紹:
- 上圖透露了幾個關鍵資訊:
- Spring Cloud Gateway內建了斷路器filter,
- 具體做法是使用Spring Cloud斷路器的API,將gateway的路由邏輯封裝到斷路器中
- 有多個斷路器的庫都可以用在Spring Cloud Gateway(遺憾的是沒有列舉是哪些)
- Resilience4J對Spring Cloud 來說是開箱即用的
- 簡單來說Spring Cloud Gateway的斷路器功能是通過內建filter實現的,這個filter使用了Spring Cloud斷路器;
- 官方說多個斷路器的庫都可以用在Spring Cloud Gateway,但是並沒有說具體是哪些,這就鬱悶了,此時我們們去了解一位牛人的觀點:Piotr Mińkowski,就是下面這本書的作者:
- Piotr Mińkowski的部落格對Spring Cloud Gateway的斷路器功能做了詳細介紹,如下圖,有幾個重要資訊稍後會提到:
- 上圖可以get到三個關鍵資訊:
- 從2.2.1版本起,Spring Cloud Gateway整合了Resilience4J的斷路器實現
- Netflix的Hystrix進入了維護階段(能理解為即將退休嗎?)
- Netflix的Hystrix依然可用,但是已廢棄(deprecated),而且Spring Cloud將來的版本可能會不支援
- 再關聯到官方文件也以resilience4為例(如下圖),膽小的我似乎沒有別的選擇了,就Resilience4J吧:
- 理論分析就到此吧,接下來開始實戰,具體的步驟如下:
- 準備工作:服務提供者新增一個web介面<font color="blue">/account/{id}</font>,根據入參的不同,該介面可以立即返回或者延時500毫秒返回
- 新增名為<font color="blue">circuitbreaker-gateway</font>的子工程,這是個帶有斷路器功能的Spring Cloud Gateway應用
- 在<font color="blue">circuitbreaker-gateway</font>裡面編寫單元測試程式碼,用來驗證斷路器是否正常
- 執行單元測試程式碼,觀察斷路器是否生效
- 給斷路器新增fallback並驗證是否生效
- 做一次簡單的原始碼分析,一為想深入瞭解斷路器的同學捋清楚原始碼路徑,二為檢驗自己以前瞭解的springboot知識在閱讀原始碼時有麼有幫助
原始碼下載
- 本篇實戰中的完整原始碼可在GitHub下載到,地址和連結資訊如下表所示(https://github.com/zq2599/blo...):
名稱 | 連結 | 備註 |
---|---|---|
專案主頁 | https://github.com/zq2599/blo... | 該專案在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blo... | 該專案原始碼的倉庫地址,https協議 |
git倉庫地址(ssh) | git@github.com:zq2599/blog_demos.git | 該專案原始碼的倉庫地址,ssh協議 |
- 這個git專案中有多個資料夾,本篇的原始碼在<font color="blue">spring-cloud-tutorials</font>資料夾下,如下圖紅框所示:
- <font color="blue">spring-cloud-tutorials</font>資料夾下有多個子工程,本篇的程式碼是<font color="red">circuitbreaker-gateway</font>,如下圖紅框所示:
準備工作
- 我們們要準備一個可控的web介面,通過引數控制它成功或者失敗,這樣才能觸發斷路器
- 本篇的實戰中,服務提供者依舊是<font color="blue">provider-hello</font>,為了滿足本次實戰的需求,我們們在Hello.java檔案中增加一個web介面,對應的原始碼如下:
@RequestMapping(value = "/account/{id}", method = RequestMethod.GET)
public String account(@PathVariable("id") int id) throws InterruptedException {
if(1==id) {
Thread.sleep(500);
}
return Constants.ACCOUNT_PREFIX + dateStr();
}
- 上述程式碼很簡單:就是接收id引數,如果等於1就延時五百毫秒,不等於1就立即返回
- 如果把斷路器設定為超過兩百毫秒就算失敗,那麼通過控制id引數的值,我們們就能模擬請求成功或者失敗了,<font color="blue">這是驗證斷路器功能的關鍵</font>
- 準備完成,開始寫程式碼
實戰
- 在父工程<font color="blue">spring-cloud-tutorials</font>下面新增子工程<font color="blue">circuitbreaker-gateway</font>
- 增加以下依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
- 配置檔案application.yml如下:
server:
#服務埠
port: 8081
spring:
application:
name: circuitbreaker-gateway
cloud:
gateway:
routes:
- id: path_route
uri: http://127.0.0.1:8082
predicates:
- Path=/hello/**
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
- 啟動類:
package com.bolingcavalry.circuitbreakergateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class CircuitbreakerApplication {
public static void main(String[] args) {
SpringApplication.run(CircuitbreakerApplication.class,args);
}
}
- 配置類如下,這是斷路器相關的引數配置:
package com.bolingcavalry.circuitbreakergateway.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class CustomizeCircuitBreakerConfig {
@Bean
public ReactiveResilience4JCircuitBreakerFactory defaultCustomizer() {
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom() //
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED) // 滑動視窗的型別為時間視窗
.slidingWindowSize(10) // 時間視窗的大小為60秒
.minimumNumberOfCalls(5) // 在單位時間視窗內最少需要5次呼叫才能開始進行統計計算
.failureRateThreshold(50) // 在單位時間視窗內呼叫失敗率達到50%後會啟動斷路器
.enableAutomaticTransitionFromOpenToHalfOpen() // 允許斷路器自動由開啟狀態轉換為半開狀態
.permittedNumberOfCallsInHalfOpenState(5) // 在半開狀態下允許進行正常呼叫的次數
.waitDurationInOpenState(Duration.ofSeconds(5)) // 斷路器開啟狀態轉換為半開狀態需要等待60秒
.recordExceptions(Throwable.class) // 所有異常都當作失敗來處理
.build();
ReactiveResilience4JCircuitBreakerFactory factory = new ReactiveResilience4JCircuitBreakerFactory();
factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(200)).build())
.circuitBreakerConfig(circuitBreakerConfig).build());
return factory;
}
}
- 上述程式碼有一次需要注意:<font color="blue">timeLimiterConfig</font>方法設定了超時時間,服務提供者如果超過200毫秒沒有響應,Spring Cloud Gateway就會向呼叫者返回失敗
- 開發完成了,接下來要考慮的是如何驗證
單元測試類
- 為了驗證Spring Cloud Gateway的斷路器功能,我們們可以用Junit單元測試來精確控制請求引數和請求次數,測試類如下,可見測試類會連續發一百次請求,在前五十次中,請求引數始終在0和1之間切換,引數等於1的時候,介面會有500毫秒延時,超過了Spring Cloud Gateway的200毫秒超時限制,這時候就會返回失敗,等失敗多了,就會觸發斷路器的斷開:
package com.bolingcavalry.circuitbreakergateway;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.reactive.server.WebTestClient;
@SpringBootTest
@ExtendWith(SpringExtension.class)
@AutoConfigureWebTestClient
public class CircuitbreakerTest {
// 測試的總次數
private static int i=0;
@Autowired
private WebTestClient webClient;
@Test
@RepeatedTest(100)
void testHelloPredicates() throws InterruptedException {
// 低於50次時,gen在0和1之間切換,也就是一次正常一次超時,
// 超過50次時,gen固定為0,此時每個請求都不會超時
int gen = (i<50) ? (i % 2) : 0;
// 次數加一
i++;
final String tag = "[" + i + "]";
// 發起web請求
webClient.get()
.uri("/hello/account/" + gen)
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectBody(String.class).consumeWith(result -> System.out.println(tag + result.getRawStatusCode() + " - " + result.getResponseBody()));
Thread.sleep(1000);
}
}
驗證
- 啟動nacos(服務提供者依賴的)
- 啟動子工程<font color="blue">provider-hello</font>
- 執行我們們剛才開發的單元測試類,控制檯輸入的內容擷取部分如下,稍後會有分析:
[2]504 - {"timestamp":"2021-08-28T02:55:42.920+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"594efed1"}
[3]200 - Account2021-08-28 10:55:43
[4]504 - {"timestamp":"2021-08-28T02:55:45.177+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"427720b"}
[5]200 - Account2021-08-28 10:55:46
[6]503 - {"timestamp":"2021-08-28T02:55:47.227+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"6595d7f4"}
[7]503 - {"timestamp":"2021-08-28T02:55:48.250+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"169ae1c"}
[8]503 - {"timestamp":"2021-08-28T02:55:49.259+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"53b695a1"}
[9]503 - {"timestamp":"2021-08-28T02:55:50.269+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"4a072f52"}
[10]504 - {"timestamp":"2021-08-28T02:55:51.499+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4bdd96c4"}
[11]200 - Account2021-08-28 10:55:52
[12]504 - {"timestamp":"2021-08-28T02:55:53.745+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4e0e7eab"}
[13]200 - Account2021-08-28 10:55:54
[14]504 - {"timestamp":"2021-08-28T02:55:56.013+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"27685405"}
[15]503 - {"timestamp":"2021-08-28T02:55:57.035+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"3e40c5db"}
[16]503 - {"timestamp":"2021-08-28T02:55:58.053+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"2bf2698b"}
[17]503 - {"timestamp":"2021-08-28T02:55:59.075+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"38cb1840"}
[18]503 - {"timestamp":"2021-08-28T02:56:00.091+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"21586fa"}
[19]200 - Account2021-08-28 10:56:01
[20]504 - {"timestamp":"2021-08-28T02:56:02.325+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"4014d6d4"}
[21]200 - Account2021-08-28 10:56:03
[22]504 - {"timestamp":"2021-08-28T02:56:04.557+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"173a3b9d"}
[23]200 - Account2021-08-28 10:56:05
[24]504 - {"timestamp":"2021-08-28T02:56:06.811+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"aa8761f"}
[25]200 - Account2021-08-28 10:56:07
[26]504 - {"timestamp":"2021-08-28T02:56:09.057+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"769bfefc"}
[27]200 - Account2021-08-28 10:56:10
[28]504 - {"timestamp":"2021-08-28T02:56:11.314+00:00","path":"/hello/account/1","status":504,"error":"Gateway Timeout","message":"","requestId":"2fbcb6c0"}
[29]503 - {"timestamp":"2021-08-28T02:56:12.332+00:00","path":"/hello/account/0","status":503,"error":"Service Unavailable","message":"","requestId":"58e4e70f"}
[30]503 - {"timestamp":"2021-08-28T02:56:13.342+00:00","path":"/hello/account/1","status":503,"error":"Service Unavailable","message":"","requestId":"367651c5"}
- 分析上述輸出的返回碼:
- 504是超時返回的錯誤,200是服務提供者的正常返回
- 504和200兩種返回碼都表示請求到達了服務提供者,所以此時斷路器是關閉狀態
- 多次504錯誤後,達到了配置的門限,觸發斷路器開啟
- 連續出現的503就是斷路器開啟後的返回碼,此時請求是無法到達服務提供者的
- 連續的503之後,504和200再次交替出現,證明此時進入半開狀態,然後504再次達到門限觸發斷路器從半開轉為開啟,五十次之後,由於不在傳送超時請求,斷路器進入關閉狀態
fallback
- 通過上述測試可見,Spring Cloud Gateway通過返回碼來告知呼叫者錯誤資訊,這種方式不夠友好,我們可以自定義fallback,在返回錯誤時由它來構建返回資訊
- 再開發一個web介面,沒錯,就是在<font color="blue">circuitbreaker-gateway</font>工程中新增一個web介面:
package com.bolingcavalry.circuitbreakergateway.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
@RestController
public class Fallback {
private String dateStr(){
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date());
}
/**
* 返回字串型別
* @return
*/
@GetMapping("/myfallback")
public String helloStr() {
return "myfallback, " + dateStr();
}
}
- application.yml配置如下,可見是給filter增加了<font color="blue">fallbackUri</font>屬性:
server:
#服務埠
port: 8081
spring:
application:
name: circuitbreaker-gateway
cloud:
gateway:
routes:
- id: path_route
uri: http://127.0.0.1:8082
predicates:
- Path=/hello/**
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/myfallback
- 再執行單元測試,可見返回碼全部是200,原來的錯誤現在全部變成了剛才新增的介面的返回內容:
[2]200 - myfallback, 2021-08-28 11:15:02
[3]200 - Account2021-08-28 11:15:03
[4]200 - myfallback, 2021-08-28 11:15:04
[5]200 - Account2021-08-28 11:15:05
[6]200 - myfallback, 2021-08-28 11:15:06
[7]200 - myfallback, 2021-08-28 11:15:08
[8]200 - myfallback, 2021-08-28 11:15:09
[9]200 - myfallback, 2021-08-28 11:15:10
[10]200 - myfallback, 2021-08-28 11:15:11
[11]200 - Account2021-08-28 11:15:12
[12]200 - myfallback, 2021-08-28 11:15:13
[13]200 - Account2021-08-28 11:15:14
[14]200 - myfallback, 2021-08-28 11:15:15
- 至此,我們們已完成了Spring Cloud Gateway的斷路器功能的開發和測試,如果聰明好學的您並不滿足這寥寥幾行配置和程式碼,想要深入瞭解斷路器的內部,那麼請您接往下看,我們們聊聊它的原始碼;
原始碼分析
- RouteDefinitionRouteLocator的構造方法(bean注入)中有如下程式碼,將name和例項繫結:
gatewayFilterFactories.forEach(factory -> this.gatewayFilterFactories.put(factory.name(), factory));
- 然後會在loadGatewayFilters方法中使用這個map,找到上面put的bean;
- 最終的效果:路由配置中指定了name等於<font color="blue">CircuitBreaker</font>,即可對應SpringCloudCircuitBreakerFilterFactory型別的bean,因為它的name方法返回了"CircuitBreaker",如下圖:
- 現在的問題:SpringCloudCircuitBreakerFilterFactory型別的bean是什麼?如下圖紅框,SpringCloudCircuitBreakerResilience4JFilterFactory是SpringCloudCircuitBreakerFilterFactory唯一的子類:
- 從上圖來看,CircuitBreaker型別的filter應該是SpringCloudCircuitBreakerResilience4JFilterFactory,不過那只是從繼承關係推斷出來的,還差一個關鍵證據:在spring中,到底存不存在SpringCloudCircuitBreakerResilience4JFilterFactory型別的bean?
- 最終發現了GatewayResilience4JCircuitBreakerAutoConfiguration中的配置,可以證明SpringCloudCircuitBreakerResilience4JFilterFactory會被例項化並註冊到spring:
@Bean
@ConditionalOnBean(ReactiveResilience4JCircuitBreakerFactory.class)
@ConditionalOnEnabledFilter
public SpringCloudCircuitBreakerResilience4JFilterFactory springCloudCircuitBreakerResilience4JFilterFactory(
ReactiveResilience4JCircuitBreakerFactory reactiveCircuitBreakerFactory,
ObjectProvider<DispatcherHandler> dispatcherHandler) {
return new SpringCloudCircuitBreakerResilience4JFilterFactory(reactiveCircuitBreakerFactory, dispatcherHandler);
}
- 綜上所述,當您配置了CircuitBreaker過濾器時,實際上是SpringCloudCircuitBreakerResilience4JFilterFactory類在為您服務,而關鍵程式碼都集中在其父類SpringCloudCircuitBreakerFilterFactory中;
- 所以,要想深入瞭解Spring Cloud Gateway的斷路器功能,請閱讀SpringCloudCircuitBreakerFilterFactory.apply方法
一點遺憾
- 還記得剛才分析控制檯輸出的那段內容嗎?就是下圖紅框中的那段,當時我們們用返回碼來推測斷路器處於什麼狀態:
- 相信您在看這段純文字時,對欣宸的分析還是存在疑惑的,根據返回碼就把斷路器的狀態確定了?例如504的時候到底是關閉還是半開呢?都有可能吧,所以,這種推測只能證明斷路器正在工作,但是無法確定某個時刻具體的狀態
- 所以,我們們需要一種更準確的方式知道每個時刻斷路器的狀態,這樣才算對斷路器有了深刻了解
- 接下來的文章中,我們們在今天的成果上更進一步,在請求中把斷路器狀態列印出來,那就...敬請期待吧,欣宸原創,從未讓您失望;
你不孤單,欣宸原創一路相伴
歡迎關注公眾號:程式設計師欣宸
微信搜尋「程式設計師欣宸」,我是欣宸,期待與您一同暢遊Java世界...
https://github.com/zq2599/blog_demos