open feign 呼叫超時與重試

KerryWu發表於2023-02-17

1. 前言

在 spring cloud 各種元件中,我最早接觸的就是 open feign,但從來沒有講過它。原因是因為覺得它簡單,無非就是個服務呼叫,在程式碼層面上也很簡單,沒有啥可說的。

但為什麼今天來講呢:

  1. 服務呼叫看起來簡單,但實則是微服務治理中很重要的一環。我們現在微服務有上百個,如何提高微服務之間呼叫的穩定性,是老大難的問題。網路或高併發等原因,幾乎每天都有個別報錯是 feign 呼叫的。
  2. open feign 其實是封裝了負載均衡、熔斷等其他元件的,掌握它是有難度的。

不過這裡著重講feign超時、重試的部分,其他的功能可以看feign官方文件

1. feign 與 openfeign

feign 是 netflix 公司寫的,是 spring cloud 元件中的一個輕量級 RESTful 的 HTTP 服務客戶端,是 spring cloud 中的第一代負載均衡客戶端。後來 netflix 講 feign 開源給了 spring cloud 社群,也隨之停更。

openfeig 是 spring cloud 自己研發的,在 feign
的基礎上支援了 spring MVC 的註解,如 @RequesMapping 等等。是 spring cloud中的第二代負載均衡客戶端。

雖然 feign 停更了,之前我也介紹過 dobbo 這類替代產品。但在服務呼叫這個領域,open feign 還是有它的一席之地。

本文中講的都是 openfeign,有些地方就簡寫成feign了。

2. spring cloud 版本更迭

先講講 spring cloud 的版本迭代吧。在2020年之前,spring cloud 等版本號是按照倫敦地鐵站號命名的(ABCDEFGH):

  1. Angle
  2. Brixton
  3. Camden
  4. Dalston
  5. Edgware
  6. Finchley
  7. GreenWich
  8. Hoxton

但從2020年開始,版本號開始以年份命名,如:2020.0.1。

spring cloud 與 spring boot 版本的對應關係如下:

spring cloud 版本spring boot 版本
2022.x3.0
2021.x2.6.x、2.7.x(2021.0.3+)
2020.x2.4.x、2.5.x(2020.0.3+)
Hoxton2.2.x、2.3.x(SR5+)
GreenWich2.1.x
Finchley2.0.x
Edgware1.5.x
Dalston1.5.x
3. open feign 版本更迭

在 2020.x 版本之前,open feign 預設依賴了 hystrix、ribbon。

但從 2020.x 版本開始,open feign 就不再依賴 hystrix、ribbon了。

  • 熔斷:可以自己選擇熔斷元件,不過需要額外引入依賴,如:resilience4j、sentinel。
  • 負載均衡:該用 spring cloud loadbalancer 替代 ribbon。

2. 示例程式碼

本著實踐出真知的原則,我們還是建立個專案試驗一下。這一章節就是把示例程式碼的核心程式碼列出來。程式碼還是以 openfeign 的低版本為主,spring cloud 版本為 Hoxton。

示例程式碼是個多模組的專案,為了構建一個簡單的 feign 服務呼叫的場景,構建下面3個子模組:

  • eureka-server:為 feign 服務呼叫提供註冊中心。
  • demo1-app:http服務,對外提供介面,供 demo2-app 呼叫。
  • demo2-app:http服務,對外提供介面,該介面呼叫 demo1-app 的介面。

2.1. parent

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.2.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>feign-service</artifactId>
    <version>${revision}</version>
    <name>feign-service</name>
    <description>feign-service</description>
    <packaging>pom</packaging>

    <properties>
        <revision>0.0.1-SNAPSHOT</revision>
        <java.version>8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <spring-boot-starter.version>2.3.2.RELEASE</spring-boot-starter.version>
        <flatten-maven-plugin.version>1.2.7</flatten-maven-plugin.version>
        <lombok.version>1.18.24</lombok.version>
        <resilience4j.version>0.13.2</resilience4j.version>
    </properties>

    <modules>
        <module>eureka-server</module>
        <module>demo1-app</module>
        <module>demo2-app</module>
    </modules>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${spring-boot-starter.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>flatten-maven-plugin</artifactId>
                    <version>${flatten-maven-plugin.version}</version>
                    <configuration>
                        <updatePomFile>true</updatePomFile>
                        <flattenMode>clean</flattenMode>
                    </configuration>
                    <executions>
                        <execution>
                            <id>flatten</id>
                            <phase>process-resources</phase>
                            <goals>
                                <goal>flatten</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>flatten-clean</id>
                            <phase>clean</phase>
                            <goals>
                                <goal>clean</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

2.2. eureka-server

1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>feign-service</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>eureka-server</artifactId>
    <name>eureka-server</name>
    <description>eureka-server</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
2. EurekaServerApplication
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}
3. application.yml
server:
  port: 8000
spring:
  application:
    name: eureka-server
eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    fetch-registry: false

2.3. demo1-app

1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>feign-service</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo1-app</artifactId>
    <name>demo1-app</name>
    <description>demo1-app</description>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
2. Demo1AppApplication
@SpringBootApplication
@EnableDiscoveryClient
public class Demo1AppApplication {

    public static void main(String[] args) {
        SpringApplication.run(Demo1AppApplication.class, args);
    }

}
3. DemoController
@RestController
@RequestMapping
@Slf4j
public class DemoController {

    @GetMapping("hello")
    public String hello(@RequestParam Integer seconds) {
        if (seconds < 0) {
            throw new RuntimeException("時間不能為負數");
        }
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("app1: 你好!");
        return "hello";
    }
}
4. application.yml
server:
  port: 8001
spring:
  application:
    name: app1
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka

2.4. demo2-app

1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>pers.kerry</groupId>
        <artifactId>feign-service</artifactId>
        <version>${revision}</version>
        <relativePath>../pom.xml</relativePath>
    </parent>

    <groupId>pers.kerry</groupId>
    <artifactId>demo2-app</artifactId>
    <name>demo2-app</name>
    <description>demo2-app</description>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
2. Demo2AppApplication
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class Demo2AppApplication {

    public static void main(String[] args) {
        SpringApplication.run(Demo2AppApplication.class, args);
    }

}
3. DemoController
@RestController
@RequestMapping("feign")
@AllArgsConstructor
@Slf4j
public class DemoController {
    private final HelloFeign helloFeign;

    @GetMapping("hello")
    public String hello(@RequestParam Integer seconds) {
        log.info("app2: 你好!");
        return helloFeign.hello(seconds);
    }
}
4. HelloFeign
@FeignClient(name = "app1")
public interface HelloFeign {

    @GetMapping("hello")
    String hello(@RequestParam Integer seconds);

}
5. application.yml
server:
  port: 8002
spring:
  application:
    name: app2

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka

3. feign 超時、重試

3.1. feign 超時

1. 設定

當我們在 demo2-app 的 application.yml 檔案中僅新增 feign 的配置:

server:
  port: 8002
spring:
  application:
    name: app2

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
feign:
  client:
    config:
      default:
        connect-timeout: 1000
        read-timeout: 2500

spring 配置類中註冊bean:

    @Bean
    public Retryer retryer(){
        return new Retryer.Default(100,1000,3);
    }

feign 的重試是透過 retryer 屬性實現的,但如果需要自定義重試策略,則需要寫程式碼註冊 bean。

按照 Retryer 類構造方法中引數順序依次為:

  • period: 初始重試間隔 ,預設實現值是 100 ms
  • maxPeriod: 最大重試間隔 ,預設實現值是 1000 ms
  • maxAttempts: 最大重試次數,初始呼叫算一次,預設實現值是 5
2. 測試用例1

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印1次數(“app1: 你好!”)
  2. app2 成功返回 “hello”
3. 測試用例2

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印3次數(“app1: 你好!”)後
  2. app2 介面報錯。錯誤:feign.RetryableException
4. 分析

在配置中,我們設定請求處理時間(readTimeout)為2.5秒,失敗後重試2次(減去初始呼叫的1次)。

在測試用例1中,因為設定 app1 處理時間在2秒,沒有超過2.5秒,所以正常請求成功,app1只列印了1次。

在測試用例2中,因為設定 app1 處理時間在3秒,超過了2.5秒,單次請求失敗,觸發了失敗重試機制。首次執行了1次,又重試了2次,所以一共有3次呼叫,app1 共列印了3次。

5. feign配置
  • connect-timeout: 請求連線的超時時間(毫秒)
  • read-timeout: 請求處理的超時時間(毫秒)
  • retryer: 重試的實現類(如:feign.Retryer.Default)。如果不配置,則預設不重試
6. 區域性配置

其實正常的配置字首應該叫 feign.config.client.${feignName}。可以針對不同的 feign 呼叫服務(@FeignClient 中的 name 屬性值),配置不同的策略。上述的 feign.config.client.default 是設定預設配置。

如下列可針對 app1、appx 配置不同策略:

feign:
  client:
    config:
      app1:
        connect-timeout: 1000
        read-timeout: 2500
        retryer: feign.Retryer.Default
      appx:
        connect-timeout: 1000
        read-timeout: 4500
        retryer: pers.kerry.demo2app.config.AppXRetryer

3.2. feign 重試(retryer)

1. 全域性配置

上述中在 配置類(@Configuration) 中註冊 Retryer Bean,就是全域性配置,所有服務都走這同一個策略。如下:

@SpringBootConfiguration
public class AppFeignConfig {
    @Bean
    public Retryer retryer(){
        return new Retryer.Default(100,1000,3);
    }
}

要注意的是,一旦在註冊了 bean,就算 feign.config.client.${feignName}.retryer 為空,也不會關閉重試策略,依然生效。所以這種方式要慎重!

2. 區域性配置(指定Bean配置類)

和上面的例子很像,同樣在類中宣告 Retryer Bean,但並非在配置類中,只是作為 feign client 指定的邏輯上“配置類”。如下:

public class AppFeignConfig {
    @Bean
    public Retryer retryer(){
        return new Retryer.Default(100,1000,3);
    }
}

然後在 HelloFeign.java 中指定配置類:

@FeignClient(name = "app1",configuration = AppFeignConfig.class)
public interface HelloFeign {

    @GetMapping("hello")
    String hello(@RequestParam Integer seconds);

}

此時 feign.config.client.${feignName}.retryer 可以為空,因為讀的是 @FeignClient 的配置了。

3. 區域性配置(指定類路徑)

可自定義類繼承 Retryer預設類(feign.Retryer.Default),可透過設定預設構造方法,來定義重試規則,如下:

pers.kerry.demo2app.config.AppXRetryer.java

public class AppXRetryer extends Retryer.Default {
    private static final int maxAttempts = 2;
    private static final long period = 100;
    private static final long maxPeriod = 1500;

    public AppXRetryer() {
        super(period, maxPeriod, maxAttempts);
    }

    @Override
    public Retryer clone() {
        return new AppXRetryer();
    }
    
}

其在 application.yml 上配置的方式是:

feign:
  client:
    config:
      default:
        connect-timeout: 1000
        read-timeout: 1500
        retryer: pers.kerry.demo2app.config.AppXRetryer

4. ribbon 超時、重試

1. 設定

當我們在 demo2-app 的 application.yml 檔案中僅新增 ribbon 的配置:

server:
  port: 8002
spring:
  application:
    name: app2
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
feign:
  hystrix:
    enabled: false
ribbon:
  ConnectTimeout: 1000
  ReadTimeout: 2500
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 0
2. 測試用例1

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印1次數(“app1: 你好!”)
  2. app2 成功返回 “hello”
3. 測試用例2

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印4次數(“app1: 你好!”)後
  2. app2 介面報錯。錯誤:feign.RetryableException
4. 分析

在配置中,我們設定請求處理時間(ReadTimeout)為2.5秒,失敗後重試3次。

在測試用例1中,因為設定 app1 處理時間在2秒,沒有超過2.5秒,所以正常請求成功,app1只列印了1次。

在測試用例2中,因為設定 app1 處理時間在3秒,超過了2.5秒,單次請求失敗,觸發了失敗重試機制。因為首次執行了1次,又重試了3次,所以一共有4次呼叫,app1 共列印了4次。

5. ribbon 配置
  • ConnectTimeout: 請求連線的超時時間(毫秒)
  • ReadTimeout: 請求處理的超時時間(毫秒)
  • MaxAutoRetries: 同一例項最大重試次數,不包括首次呼叫。預設值為0
  • MaxAutoRetriesNextServer: 同一個服務其他例項的最大重試次數,不包括第一次呼叫的例項。預設值為1
  • OkToRetryOnAllOperations: 是否所有操作都允許重試。預設值為false,即只在GET協議上重試所有錯誤
  • ServerListRefreshInterval: Ribbon更新服務註冊列表的頻率(毫秒)
6. 區域性配置

可針對不用的 feign 呼叫服務(@FeignClient 中的 name 屬性值),配置不同的策略。如,下列可針對 app1、appx 配置不同策略:

app1:
  ribbon:
    ConnectTimeout: 1000
    ReadTimeout: 2500
    MaxAutoRetries: 3
    MaxAutoRetriesNextServer: 0
appn:
  ribbon:
    ConnectTimeout: 1000
    ReadTimeout: 4500
    MaxAutoRetries: 0
    MaxAutoRetriesNextServer: 3

5. feign、ribbon 比較

1. 比較

feign 的配置策略更豐富,至少 idea 會有提示。

但在失敗重試的方向上,ribbon功能更強大。不僅是配置起來更簡單,而且支援跨服務重試,這個在實際應用中很重要。畢竟當某個服務因高併發而短暫阻塞時,最好的解決方法就是引流到其他服務上重試。

2. 優先順序

當上述 application 檔案中,feign、ribbon 同時開啟配置如下:

feign:
  hystrix:
    enabled: false
  client:
    config:
      default:
        connect-timeout: 1000
        read-timeout: 1500
        retryer: pers.kerry.demo2app.config.AppXRetryer
ribbon:
  ConnectTimeout: 1000
  ReadTimeout: 2500
  MaxAutoRetries: 3
  MaxAutoRetriesNextServer: 0

在測試時發現無論超時還是重試,當前生效的只有 feign 的配置。

可見預設情況下,feign 配置的優先順序要高於 ribbon。

因為有一個 feign.client.default-to-properties 的屬性,其作用是初始化物件獲取屬性的優先順序順序。因為預設值為true,即 feign配置的優先順序最高。如果手動設定為 false,則可以以 ribbon 的配置生效。

6. 熔斷 hystrix(feign低版本)

hystrix 是由 netflix 開源的一款容錯框架,包含隔離(執行緒池隔離、訊號量隔離)、熔斷、降級回退和快取容錯、快取、批次處理請求、主從分擔等常用功能。

feign本身支援 hystrix,預設是關閉 hystrix 的,需要在配置檔案中開啟 feign.hystrix.enabled=true,預設值為 false。

6.1. hystrix 測試

因為 feign 預設就引入了 hystrix,在開啟 feign.hystrix 後,只需要設定 hystrix 的配置就可以了。如下面的配置,再測試一下:

feign:
  client:
    config:
      default:
        connect-timeout: 1000
        read-timeout: 1500
        retryer: pers.kerry.demo2app.config.AppXRetryer
    default-to-properties: true
  hystrix:
    enabled: true

hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: true
        isolation:
          strategy: THREAD
          thread:
            timeoutInMilliseconds: 2500
1. 測試用例1

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印1次數(“app1: 你好!”)
  2. app2 成功返回 “hello”
2. 測試用例2

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印2次數(“app1: 你好!”)後
  2. app2 介面報錯。錯誤:com.netflix.hystrix.exception.HystrixRuntimeException
3. 測試用例3

呼叫介面:(GET) http://localhost:8002/feign/h...

執行結果按照時間順序是:

  1. app1 列印1次數(“app1: 你好!”)
  2. app2 介面報錯。錯誤:com.netflix.hystrix.exception.HystrixRuntimeException
  3. app1 再列印1次數(“app1: 你好!”)

如果不考慮 hystrix 的因素,當請求 seconds 值為3時,應該是和值為2時一樣,在重試1次後再中斷請求報錯。

但由於3大於 hystrix 設定的超時時間2.5,在第一次請求時就觸發了熔斷報錯。不過由於 feign 的重試機制,依然再重試了1次,但屬於無效的重試,畢竟app2介面的http請求已經終結了。

所以如果需要開啟 hystrix 熔斷,各自超時時間的值,需要好好搭配一下。

6.2. hystrix 配置

當開啟 feign.hystrix 後,可參考下列預設配置。

hystrix:
  command:
    default:
      execution:
        timeout:
          enabled: true # 開啟超時熔斷
        isolation:
          strategy: THREAD
          semaphore:
            maxConcurrentRequests: 100 # 預設最大100個訊號量併發,業務可根據具體情況調整(strategy=semaphore時生效)
          thread:
            timeoutInMilliseconds: 10000 # 預設熔斷時間10秒,需要大於ribbon的retry*timeout
      #熔斷策略
      circuitBreaker:
        enabled: true # 啟用熔斷
        requestVolumeThreshold: 20 # 度量視窗內請求量閾值,熔斷前置條件,預設20
        errorThresholdPercentage: 50 # 錯誤閾值比例,超過則觸發熔斷,預設50%
        sleepWindowInMilliseconds: 5000 # 等待時間後重新檢查請求,預設5秒
  threadpool:
    default:
      coreSize: 10 # 核心數量,預設10,可根據實際業務調整
      maximumSize: 10 # 最大數量,預設10,可根據實際業務調整
      allowMaximumSizeToDivergeFromCoreSize: true # 是否允許從coreSize擴充到maximumSize
      maxQueueSize: 1000 # 佇列最大數量,不支援動態配置
      queueSizeRejectionThreshold: 500 # 佇列數量閾值,可動態配置
      keepAliveTimeMinutes: 2

實際的配置項可看 hystrix 官方文件。本文要特別強調的是:

hystrix 超時時長 > (ribbon 超時時長 ribbon 重試次數) or (feign 超時時長 feign 重試次數 )

7. 熔斷 sentinel(feign高版本)

1. 配置

先在 demo2-app 的pom中引入sentinel 的依賴:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

啟動本地 sentinel dashboard 服務,便於觀察 feign的熔斷策略。
在 application 檔案中加上對dashboard的註冊,並且開啟 feign.sentinel:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 127.0.0.1:9999
        port: 8721
feign:
  sentinel:
    enabled: true

當開啟 dashboard,發現feign中對應hello 的方法,對應的資源名稱是 GET:http://app1/hello,如果不開啟 feign.sentinel.enabled,該資源不會出來。

這樣,我們就可以基於dashboard上的該資源,對其建立規則了。

為了方便文章展示,這裡透過程式碼中註冊bean的方式來建立規則,和直接在dashboard中配置是一樣的。

@SpringBootConfiguration
public class AppFeignConfig {

    @Bean
    private static void initDegradeRule() {
        List<DegradeRule> rules = new ArrayList<>();
        DegradeRule rule = new DegradeRule("GET:http://app1/hello")
                .setGrade(CircuitBreakerStrategy.ERROR_COUNT.getType())
                .setCount(5)
                .setMinRequestAmount(2)
                .setStatIntervalMs(1000)
                .setTimeWindow(10);
        rules.add(rule);
        DegradeRuleManager.loadRules(rules);
    }
}

這裡配置了一個熔斷器的規則,當一秒內有5個報錯,就回觸發熔斷。當10秒後會半開,半開期如果下一次依然報錯,則再次熔斷。可如果正常了,就關閉熔斷器。

feign中新增降級類:

@FeignClient(name = "app1", fallback = FallbackHelloFeign.class)
... ...

FallbackHelloFeign 類中定義降級方法:

@Component
public class FallbackHelloFeign implements HelloFeign{
    @Override
    public String hello(Integer seconds) {
        return "fallback hello";
    }
}

controller 修改一下,方便測試驗證:

    @GetMapping("hello")
    public String hello(@RequestParam Integer seconds) throws Exception{
        for (int i = 0; i < seconds; i++) {
           helloFeign.hello(-1);
        }
        log.info("app2: 你好!");
        return helloFeign.hello(0);
    }
2. 驗證

當服務啟動後,在 dashboard 中能看到之前在 bean 中定義的熔斷規則。

第1次呼叫,傳入 seconds=3,因為小於閾值,正常返回 hello。

第2次測試,傳入 seconds=6,此時大於閾值,觸發熔斷,返回 fallback hello。

因為熔斷了,在熔斷後的10秒內,就算傳入 seconds=0,不產生報錯,也只會返回 fallback hello。

當10秒後,傳入 seconds=1,雖然就報錯1次,但由於是半開階段,再次熔斷。

再等10秒後,再次傳入 seconds=0,此時因為不報錯了,發現熔斷解除了,恢復到剛開始。

3. 動態生成規則

重新回到之前的資源名稱 GET:http://app1/hello,可以發現是有規律的。我們完全可以透過反射,拼接獲取到feign中每個方法的資源名稱。

有了資源名稱,就可以動態載入到 sentinel 的規則中。

相關文章