12.SpringCloudAlibabaSentinel實現熔斷和限流

长名06發表於2024-11-28

1.Sentinel

1.1 官網

sentinel官網,類似Spring Cloud Circuit Breaker

1.2 是什麼

面向分散式、多語言異構化服務架構的流量治理元件。

1.3 下載地址

https://github.com/alibaba/Sentinel/releases

1.4 能幹嗎

Sentinel以流量為切入點,從流量控制、流量路由、熔斷降級、系統自適應過載保護、熱點流量防護等多個維度保護服務的穩定性。

Sentinel的特徵:

豐富的應用場景:Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即突發流量控制在系統容量可以承受的範圍)、訊息削峰填谷、叢集流量控制、實時熔斷下游不可用應用等。

完備的實時監控:Sentinel 同時提供實時的監控功能。您可以在控制檯中看到接入應用的單臺機器秒級資料,甚至 500 臺以下規模的叢集的彙總執行情況。

廣泛的開源生態:Sentinel 提供開箱即用的與其它開源框架/庫的整合模組,例如與 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入 Sentinel。同時 Sentinel 提供 Java/Go/C++ 等多語言的原生實現。

完善的 SPI 擴充套件機制:Sentinel 提供簡單易用、完善的 SPI 擴充套件介面。您可以透過實現擴充套件介面來快速地定製邏輯。例如定製規則管理、適配動態資料來源等。

Sentinel的主要特性:

2.服務雪崩、降級、熔斷、限流、隔離、超時區別

2.1 服務雪崩

多個微服務之間呼叫的時候,假設微服務A呼叫微服務B和微服務C,微服務B和微服務C又呼叫其它的微服務,這就是所謂的“扇出”。如果扇出的鏈路上某個微服務的呼叫響應時間過長或者不可用,對微服務A的呼叫就會佔用越來越多的系統資源,進而引起系統崩潰,所謂的“雪崩效應”。對於高流量的應用來說,單一的後端依賴可能會導致所有伺服器上的所有資源都在幾秒鐘內飽和。比失敗更糟糕的是,這些應用程式還可能導致服務之間的延遲增加,備份佇列,執行緒和其他系統資源緊張,導致整個系統發生更多的級聯故障。這些都表示需要對故障和延遲進行隔離和管理,以便單個依賴關係的失敗,不能取消整個應用程式或系統。

所以,通常當你發現一個模組下的某個例項失敗後,這時候這個模組依然還會接收流量,然後這個有問題的模組還呼叫了其他的模組,這樣就會發生級聯故障,或者叫雪崩。複雜分散式體系結構中的應用程式有數十個依賴關係,每個依賴關係在某些時候將不可避免地失敗。

2.2 服務降級

服務降級,說白了就是一種服務託底方案,如果服務無法完成正常的呼叫流程,就使用預設的託底方案來返回資料。

例如,在商品詳情頁一般都會展示商品的介紹資訊,一旦商品詳情頁系統出現故障無法呼叫時,會直接獲取快取中的商品介紹資訊返回給前端頁面。

2.3 服務熔斷

在分散式與微服務系統中,如果下游服務因為訪問壓力過大導致響應很慢或者一直呼叫失敗時,上游服務為了保證系統的整體可用性,會暫時斷開與下游服務的呼叫連線。這種方式就是熔斷。類比保險絲達到最大服務訪問後,直接拒絕訪問,拉閘限電,然後呼叫服務降級的方法並返回友好提示。

服務熔斷一般情況下會有三種狀態:閉合、開啟和半熔斷;

閉合狀態(保險絲閉合通電OK):服務一切正常,沒有故障時,上游服務呼叫下游服務時,不會有任何限制。

開啟狀態(保險絲斷開通電Error):上游服務不再呼叫下游服務的介面,會直接返回上游服務中預定的方法。

半熔斷狀態:處於開啟狀態時,上游服務會根據一定的規則,嘗試恢復對下游服務的呼叫。此時,上游服務會以有限的流量來呼叫下游服務,同時,會監控呼叫的成功率。如果成功率達到預期,則進入關閉狀態。如果未達到預期,會重新進入開啟狀態。

2.4 服務限流

服務限流就是限制進入系統的流量,以防止進入系統的流量過大而壓垮系統。其主要的作用就是保護服務節點或者叢集后面的資料節點,防止瞬時流量過大使服務和資料崩潰(如前端快取大量實效),造成不可用;還可用於平滑請求,類似秒殺高併發等操作,嚴禁一窩蜂的過來擁擠,大家排隊,一秒鐘N個,有序進行。

限流演算法有兩種,一種就是簡單的請求總量計數,一種就是時間視窗限流(一般為1s),如令牌桶演算法和漏牌桶演算法就是時間視窗的限流演算法。

2.5 服務隔離

有點類似於系統的垂直拆分,就按照一定的規則將系統劃分成多個服務模組,並且每個服務模組之間是互相獨立的,不會存在強依賴的關係。如果某個拆分後的服務發生故障後,能夠將故障產生的影響限制在某個具體的服務內,不會向其他服務擴散,自然也就不會對整體服務產生致命的影響。

網際網路行業常用的服務隔離方式有:執行緒池隔離和訊號量隔離。

2.6 服務超時

整個系統採用分散式和微服務架構後,系統被拆分成一個個小服務,就會存在服務與服務之間互相呼叫的現象,從而形成一個個呼叫鏈。

形成呼叫鏈關係的兩個服務中,主動呼叫其他服務介面的服務處於呼叫鏈的上游,提供介面供其他服務呼叫的服務處於呼叫鏈的下游。服務超時就是在上游服務呼叫下游服務時,設定一個最大響應時間,如果超過這個最大響應時間下游服務還未返回結果,則斷開上游服務與下游服務之間的請求連線,釋放資源。

3.安裝Sentinel

3.1 Sentinel元件由2部分組成

Sentinel 分為兩個部分:

  • 核心庫(Java 客戶端)不依賴任何框架/庫,能夠執行於所有 Java 執行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支援。
  • 控制檯(Dashboard)基於 Spring Boot 開發,打包後可以直接執行,不需要額外的 Tomcat 等應用容器。

3.2 安裝步驟

3.2.1 下載

略,見上文1.3的,下載地址。

3.2.2 執行命令

前提:JDK環境OK。8080埠不能佔用。

命令:

java -jar sentinel-dashboard-1.8.6.jar
3.2.3 訪問Sentinel管理介面

http://localhost:8080

登入賬號密碼均為sentinel

4.微服務8401整合Sentinel入門案例

4.1 啟動Nacos8848

啟動命令

startup.cmd -m standalone

前臺介面

http://localhost:8848/nacos

4.2 啟動Sentinel8080

java -jar sentinel-dashboard-1.8.6.jar

建議,啟動命令,固定為start.bat檔案。

4.3 新建微服務8401

4.3.1 新建Module

cloudalibaba-sentinel-service8401

4.3.2 POM
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.atguigu.cloud</groupId>
        <artifactId>cloud</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>cloudalibaba-sentinel-service8401</artifactId>

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--SpringCloud ailibaba sentinel-datasource-nacos -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
        <!--SpringCloud alibaba sentinel -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--nacos-discovery-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- 引入自己定義的api通用包 -->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--SpringBoot通用依賴模組-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--hutool-->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
            <scope>provided</scope>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
4.3.3 YML
server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service #8401微服務提供者後續將會被納入阿里巴巴sentinel監管
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制檯服務地址
        port: 8719 #預設8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
4.3.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Main8401 {
    
    public static void main(String[] args) {
        SpringApplication.run(Main8401.class, args);
    }

}
4.3.5 業務類
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @auther zzyy
 * @create 2023-05-24 15:35
 */
@RestController
public class FlowLimitController{

    @GetMapping("/testA")
    public String testA(){
        return "------testA";
    }

    @GetMapping("/testB")
    public String testB(){
        return "------testB";
    }
}

4.4 啟動8401微服務後檢視Sentiel控制檯

Sentinel採用的懶載入說明:想使用Sentinel對某個介面進行限流和降級等操作,一定要先訪問下介面,使Sentinel檢測出相應的介面。

執行一次訪問,在Sentinel前臺介面,看到。

5.流控規則

5.1 基本介紹


Sentinel能夠對流量進行控制,主要是監控應用的QPS流量或者併發執行緒數等指標,如果達到指定的閾值時,就會被流量進行控制,以避免服務被瞬時的高併發流量擊垮,保證服務的高可靠性。引數見最下方:

名稱 作用
1資源名 資源的唯一名稱,預設就是請求的介面路徑,可以自行修改,但是要保證唯一。
2針對來源 具體針對某個微服務進行限流,預設值為default,表示不區分來源,全部限流。
3閾值型別 QPS表示透過QPS進行限流,併發執行緒數表示透過併發執行緒數限流。
4單機閾值 與閾值型別組合使用。如果閾值型別選擇的是QPS,表示當呼叫介面的QPS達到閾值時,進行限流操作。如果閾值型別選擇的是併發執行緒數,則表示當呼叫介面的併發執行緒數達到閾值時,進行限流操作。
5是否叢集 選中則表示叢集環境,不選中則表示非叢集環境。

5.2 流控模式

5.2.1 直接模式

預設的流控模式,當介面達到限流條件時,直接開啟限流功能。

配置

表示1秒鐘內查詢1次就是OK,若超過次數1,就直接-快速失敗,報預設錯誤。

測試

快速訪問http://localhost:8401/testA,結果Blocked by Sentinel (flow limiting)

思考,直接呼叫預設報錯資訊,技術方面OK,但是是否該有我們的後續處理,類似fallback的兜底方法。

5.2.2 關聯模式

當關聯的資源達到閾值時,就限流自己。

配置A


當關聯資源/testB的qps閥值超過1時,就限流/testA的Rest訪問地址,當關聯資源到閾值後限制配置好的資源名,B惹事,A掛了。

使用Jmeter模擬併發密集訪問testB

jmeter下載地址

大批次的訪問testB,在訪問testA會發現A,訪問出現Blocked by Sentinel (flow limiting)。

5.2.3 鏈路模式

來自不同鏈路的請求對同一目標訪問時,實施針對性的不同限流措施,比如C請求來訪問就限流,D請求來訪問就是OK。

修改

YML

spring:
  application:
    name: cloudalibaba-sentinel-service #8401微服務提供者後續將會被納入阿里巴巴sentinel監管
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制檯服務地址
        port: 8719 #預設8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
      web-context-unify: false # controller層的方法對service層呼叫不認為是同一個根鏈路,新增

業務類

FlowLimitController類

    /**
     * 流控-鏈路演示demo
     * C和D兩個請求都訪問flowLimitService.common()方法,閾值到達後對C限流,對D不管
     */
    @Resource
    private FlowLimitService flowLimitService;

	/**流控-鏈路演示demo
     * C和D兩個請求都訪問flowLimitService.common()方法,閾值到達後對C限流,對D不管
     */
    @GetMapping("/testC")
    public String testC() {
        flowLimitService.common();
        return "------testC";
    }

    @GetMapping("/testD")
    public String testD() {
        flowLimitService.common();
        return "------testD";
    }

FlowLimitService類

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import org.springframework.stereotype.Service;

@Service
public class FlowLimitService {
    @SentinelResource(value = "common") #該註解,後面介紹
    public void common() {
        System.out.println("------FlowLimitService come in");
    }
}

Sentinel配置

說明:C和D兩個請求都訪問flowLimitService.common()方法,對C限流,對D不管。

測試

C鏈路,超過每秒1次,限流。D鏈路OK。

5.3 流控效果

5.3.1 直接

快速失敗(預設的流控處理),直接失敗,丟擲異常,Blocked by Sentinel (flow limiting)。

5.3.2 預熱Warm Up

限流 冷啟動:當流量突然增大的時候,會希望系統從空閒狀態到繁忙狀態的切換的時間長一些。

公式:閾值除以冷卻因子coldFactor(預設值3),經過預熱時長後才會達到閾值。

官網:Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即預熱/冷啟動方式。當系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉昇到高水位可能瞬間把系統壓垮。透過"冷啟動",讓透過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,給冷系統一個預熱的時間,避免冷系統被壓垮。詳細文件可以參考 流量控制 - Warm Up 文件,具體的例子可以參見 WarmUpFlowDemo

原始碼:

public class WarmUpController implements TrafficShapingController {

    protected double count;
    private int coldFactor;
    protected int warningToken = 0;
    private int maxToken;
    protected double slope;

    protected AtomicLong storedTokens = new AtomicLong(0);
    protected AtomicLong lastFilledTime = new AtomicLong(0);

    public WarmUpController(double count, int warmUpPeriodInSec, int coldFactor) {
        construct(count, warmUpPeriodInSec, coldFactor);
    }

    public WarmUpController(double count, int warmUpPeriodInSec) {
        construct(count, warmUpPeriodInSec, 3);
    }
...
}

WarmUp配置


預設 coldFactor 為 3,即請求QPS從(threshold / 3) 開始,經多少預熱時長才逐漸升至設定的 QPS 閾值。

案例,單機閾值為10,預熱時長設定5秒。系統初始化的閾值為10 / 3 約等於3,即單機閾值剛開始為3(我們人工設定單機閾值是10,sentinel計算後QPS判定為3開始);然後過了5秒後閥值才慢慢升高恢復到設定的單機閾值10,也就是說5秒鐘內QPS為3,過了保護期5秒後QPS為10

應用場景:秒殺系統在開啟的瞬間,會有很多流量上來,很有可能把系統打死,預熱方式就是把為了保護系統,可慢慢的把流量放進來,慢慢的把閾值增長到設定的閾值。

5.3.3 排隊等待


這種方式主要用於處理間隔性突發的流量,例如訊息佇列。想象一下這樣的場景,在某一秒有大量的請求到來,而接下來的幾秒則處於空閒狀態,我們希望系統能夠在接下來的空閒期間逐漸處理這些請求,而不是在第一秒直接拒絕多餘的請求。

注意:勻速排隊模式暫時不支援 QPS > 1000 的場景。

修改FlowLimitController

@GetMapping("/testE")
public String testE()
{
    System.out.println(System.currentTimeMillis()+"      testE,排隊等待");
    return "------testE";
}


按照單機閾值,一秒鐘透過一個請求,10秒後的請求作為超時處理,放棄。

5.4 流控效果(併發執行緒數)


Jmeter模擬多個執行緒併發,迴圈請求。
Jmeter打滿了,只有Jmeter執行緒切換,系統判斷沒訪問時,才能訪問到。

6.熔斷規則

6.1 官網

https://github.com/alibaba/Sentinel/wiki/熔斷降級

6.2 基本介紹

Sentinel 熔斷降級會在呼叫鏈路中某個資源出現不穩定狀態時(例如呼叫超時或異常比例升高),對這個資源的呼叫進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。當資源被降級後,在接下來的降級時間視窗之內,對該資源的呼叫都自動熔斷(預設行為是丟擲 DegradeException)。

6.3 熔斷規則實戰

6.3.1 慢呼叫比例

慢呼叫比例 (SLOW_REQUEST_RATIO):選擇以慢呼叫比例作為閾值,需要設定允許的慢呼叫 RT(即最大的響應時間),請求的響應時間大於該值則統計為慢呼叫。當單位統計時長(statIntervalMs)內請求數目大於設定的最小請求數目,並且慢呼叫的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求響應時間小於設定的慢呼叫 RT 則結束熔斷,若大於設定的慢呼叫 RT 則會再次被熔斷。

6.3.2 名詞解釋

進入熔斷狀態判斷依據:在統計時長內,實際請求數目>設定的最小請求數且實際慢呼叫比例>比例閾值 ,進入熔斷狀態。

1.呼叫:一個請求傳送到伺服器,伺服器給與響應,一個響應就是一個呼叫。

2.最大RT(Response Time):即最大的響應時間,指系統對請求作出響應的業務處理時間。

3.慢呼叫:處理業務邏輯的實際時間>設定的最大RT時間,這個呼叫叫做慢呼叫。

4.慢呼叫比例:在所以呼叫中,慢呼叫佔有實際的比例=慢呼叫次數/總呼叫次數。

5.比例閾值:自己設定的 , 比例閾值=慢呼叫次數/呼叫次數。

6.統計時長:時間的判斷依據。

7.最小請求數:設定的呼叫最小請求數,上圖比如1秒鐘打進來10個執行緒(大於我們配置的5個了)呼叫被觸發。

6.3.3 觸發條件 + 熔斷狀態

熔斷狀態(保險絲跳閘斷電,不可訪問):在接下來的熔斷時長內請求會自動被熔斷。

探測恢復狀態(探路先鋒):熔斷時長結束後進入探測恢復狀態。

結束熔斷(保險絲閉合恢復,可以訪問):在探測恢復狀態,如果接下來的一個請求響應時間小於設定的慢呼叫 RT,則結束熔斷,否則繼續熔斷。

6.3.4 測試
/**
 * 新增熔斷規則-慢呼叫比例
 * @return
 */
@GetMapping("/testF")
public String testF()
{
    //暫停幾秒鐘執行緒
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    System.out.println("----測試:新增熔斷規則-慢呼叫比例 ");
    return "------testF 新增熔斷規則-慢呼叫比例";
}



按照上述配置,熔斷觸發:

多次迴圈,一秒鐘打進來10個執行緒(大於5個了)呼叫/testF,我們希望200毫秒處理完一次呼叫,和諧系統;

假如在統計時長內,實際請求數目>最小請求數且慢呼叫比例>比例閾值 ,斷路器開啟(保險絲跳閘)微服務不可用(Blocked by Sentinel (flow limiting)),進入熔斷狀態5秒;後續我停止jmeter,沒有這麼大的訪問量了,單獨用瀏覽器訪問rest地址,斷路器關閉(保險絲恢復,合上閘口),微服務恢復OK。

6.3.5 異常比例

異常比例 (ERROR_RATIO):當單位統計時長(statIntervalMs)內請求數目大於設定的最小請求數目,並且異常的比例大於閾值,則接下來的熔斷時長內請求會自動被熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。異常比率的閾值範圍是 [0.0, 1.0],代表 0% - 100%

6.3.6 測試
/**
 * 新增熔斷規則-異常比例
 * @return
 */
@GetMapping("/testG")
public String testG()
{
    System.out.println("----測試:新增熔斷規則-異常比例 ");
    int age = 10/0;
    return "------testG,新增熔斷規則-異常比例 ";
}


按照上述配置,單獨訪問一次,必然來一次報錯一次(int age = 10/0)達到100%,調一次錯一次報錯error;

開啟jmeter後,直接高併發傳送請求,多次呼叫達到我們的配置條件了。

斷路器開啟(保險絲跳閘),微服務不可用了,不再報錯error而是服務熔斷+服務降級,出提示Blocked by Sentinel (flow limiting)。

6.3.7 異常數

異常數 (ERROR_COUNT):當單位統計時長內的異常數目超過閾值之後會自動進行熔斷。經過熔斷時長後熔斷器會進入探測恢復狀態(HALF-OPEN 狀態),若接下來的一個請求成功完成(沒有錯誤)則結束熔斷,否則會再次被熔斷。

6.3.7 測試
/**
 * 新增熔斷規則-異常數
 * @return
 */
@GetMapping("/testH")
public String testH()
{
    System.out.println("----測試:新增熔斷規則-異常數 ");
    int age = 10/0;
    return "------testH,新增熔斷規則-異常數 ";
}


http://localhost:8401/testH
,第一次訪問絕對報錯,因為除數不能為零,我們看到error視窗;開啟jmeter後,直接高併發傳送請求,多次呼叫達到我們的配置條件了。

上述配置表示,在1秒鐘內最少請求2次,當異常數大於1時,會觸發熔斷操作斷路器開啟(保險絲跳閘),微服務不可用了,熔斷的時長為5秒,不再報錯error,而是服務降級了出提示Blocked by Sentinel (flow limiting)

7.@SentinelResource

7.1 是什麼

SentinelResource是一個流量防衛元件的註解,用於標識防護資源,方便對資源進行流量控制,熔斷降級等功能。

7.2 註解說明

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {

    //資源名稱
    String value() default "";

    //entry型別,標記流量的方向,取值IN/OUT,預設是OUT
    EntryType entryType() default EntryType.OUT;
    //資源分類
    int resourceType() default 0;

    /**
    * 處理BlockException的函式名稱,函式要求:
    * 1. 必須是 public
   	* 2.返回型別 引數與原方法一致
   	* 3. 預設需和原方法在同一個類中。若希望使用其他類的函式,可配置blockHandlerClass ,並指定blockHandlerClass裡面的方法。
    */
    String blockHandler() default "";

    //存放blockHandler的類,對應的處理函式必須static修飾。
    Class<?>[] blockHandlerClass() default {};

    /**
    * 用於在丟擲異常的時候提供fallback處理邏輯。 fallback函式可以針對所
   	* 有型別的異常(除了 exceptionsToIgnore 裡面排除掉的異常型別)進行處理。函式要求:
    * 1. 返回型別與原方法一致
    * 2. 引數型別需要和原方法相匹配
    * 3. 預設需和原方法在同一個類中。若希望使用其他類的函式,可配置fallbackClass ,並指定fallbackClass裡面的方法。
    */
    String fallback() default "";

    //存放fallback的類。對應的處理函式必須static修飾。
    String defaultFallback() default "";

    /**
    * 用於通用的 fallback 邏輯。預設fallback函式可以針對所有型別的異常進
    * 行處理。若同時配置了 fallback 和 defaultFallback,以fallback為準。函式要求:
    * 1. 返回型別與原方法一致
    * 2. 方法引數列表為空,或者有一個 Throwable 型別的引數。
    * 3. 預設需要和原方法在同一個類中。若希望使用其他類的函式,可配置fallbackClass ,並指定 fallbackClass 裡面的方法。
    */
    Class<?>[] fallbackClass() default {};
 

    //需要trace的異常
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    //指定排除忽略掉哪些異常。排除的異常不會計入異常統計,也不會進入fallback邏輯,而是原樣丟擲。
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

7.3 按照Rest地址限流 + 預設限流返回

7.3.1 業務類
RateLimitController
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @auther zzyy
 * @create 2023-05-30 16:13
 */
@RestController
@Slf4j
public class RateLimitController
{
    @GetMapping("/rateLimit/byUrl")
    public String byUrl()
    {
        return "按rest地址限流測試OK";
    }
}
7.3.2 配置

7.3.3 測試

快速點選http://localhost:8401/rateLimit/byUrl。

結果,返回Sentinel自帶的,限流處理結果Blocked by Sentinel (flow limiting)。

7.4 按SentinelResource資源名稱限流 + 自定義限流返回

7.4.1 RateLimitController修改
    @GetMapping("/rateLimit/byResource")
    @SentinelResource(value = "byResourceSentinelResource",blockHandler = "handleException")
    public String byResource()
    {
        return "按資源名稱SentinelResource限流測試OK";
    }
    public String handleException(BlockException exception)
    {
        return "服務不可用@SentinelResource啟動"+"\t"+"o(╥﹏╥)o";
    }
7.4.2 配置

7.4.3 測試

超過閾值,返回自定義限流資訊。服務不可用@SentinelResource啟動 o(╥﹏╥)o

7.5 按SentinelResource資源名稱限流 + 自定義限流返回 + 服務降級處理

7.5.1 RateLimitController修改
    @GetMapping("/rateLimit/doAction/{p1}")
    @SentinelResource(value = "doActionSentinelResource",
            blockHandler = "doActionBlockHandler", fallback = "doActionFallback")
    public String doAction(@PathVariable("p1") Integer p1) {
        if (p1 == 0){
            throw new RuntimeException("p1等於零直接異常");
        }
        return "doAction";
    }

    public String doActionBlockHandler(@PathVariable("p1") Integer p1,BlockException e){
        log.error("sentinel配置自定義限流了:{}", e);
        return "sentinel配置自定義限流了";
    }

    public String doActionFallback(@PathVariable("p1") Integer p1,Throwable e){
        log.error("程式邏輯異常了:{}", e);
        return "程式邏輯異常了"+"\t"+e.getMessage();
    }
7.5.2 配置

7.5.3 測試

http://localhost:8401/rateLimit/doAction/2
,達到閾值,出現sentinel配置自定義限流了。http://localhost:8401/rateLimit/doAction/0,返回自定義的服務降級處理。

7.6 小結

blockHandler:主要針對配置後出現的違規情況處理。

fallback:業務中出現的異常。

8.熱點規則

8.1 介紹

何為熱點

熱點即經常訪問的資料,很多時候我們希望統計或者限制某個熱點資料中訪問頻次最高的TopN資料,並對其訪問進行限流或者其它操作。

8.2 官網

https://github.com/alibaba/Sentinel/wiki/熱點引數限流

8.3 業務類修改

    @GetMapping("/testHotKey")
    @SentinelResource(value = "testHotKey", blockHandler = "dealHandler_testHotKey")
    public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
                             @RequestParam(value = "p2", required = false) String p2) {
        return "------testHotKey";
    }

    public String dealHandler_testHotKey(String p1, String p2, BlockException exception) {
        return "-----dealHandler_testHotKey";
    }

8.4 配置


限流模式只支援QPS模式,固定寫死了。(這才叫熱點)

@SentinelResource註解的方法引數索引,0代表第一個引數,1代表第二個引數,以此類推

單機閥值以及統計視窗時長表示在此視窗時間超過閥值就限流。

上面的抓圖就是第一個引數有值的話,1秒的QPS為1,超過就限流,限流後呼叫dealHandler_testHotKey支援方法。

8.5 測試

http://localhost:8401/testHotKey?p1=1
http://localhost:8401/testHotKey?p1=1&p2=2
http://localhost:8401/testHotKey?p2=2。
含有引數,p1,會觸發配置的限流。不含有引數p1,不會觸發限流操作配置。

8.6 引數例外項

配置的熱點規則,想配置,某些引數,有額外的閾值。

8.7 測試

http://localhost:8401/testHotKey?p1=5
快速重新整理,無限流提醒。
http://localhost:8401/testHotKey?p1=1
快速重新整理,有限流提醒。

前提條件,熱點引數的型別,只能時有限的基本型別(int、double、long、float、char、byte)和String

9.授權規則

9.1 介紹

在某些場景下,需要根據呼叫介面的來源判斷是否允許執行本次請求。此時就可以使用Sentinel提供的授權規則來實現,Sentinel的授權規則能夠根據請求的來源判斷是否允許本次請求透過。

在Sentinel的授權規則中,提供了 白名單與黑名單 兩種授權型別。白放行、黑禁止。

9.2 官網

https://github.com/alibaba/Sentinel/wiki/黑白名單控制

9.3 程式碼修改

package com.atguigu.cloud.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class EmpowerController //Empower授權規則,用來處理請求的來源
{
    @GetMapping(value = "/empower")
    public String requestSentinel4() {
        log.info("測試Sentinel授權規則empower");
        return "Sentinel授權規則";
    }
}


package com.atguigu.cloud.handler;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;

@Component
public class MyRequestOriginParser implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest httpServletRequest) {
        return httpServletRequest.getParameter("serverName");
    }
}

重啟,訪問http://localhost:8401/empower

9.4 配置

9.5 測試

http://localhost:8401/empower?serverName=test
http://localhost:8401/empower?serverName=test2

無法訪問,被Sentienl限流。

http://localhost:8401/empower?serverName=test1
正常訪問,不被Sentienl限流。

10.規則持久化

10.1 是什麼

現在,在Sentinel中配置的Sentinel規則,一旦重啟微服務應用,規則會消失,需要將配置規則持久化。

10.2 怎麼玩

將限流配置規則持久化進Nacos儲存,只要重新整理8401某個rest地址,sentinel控制檯的流控規則就能看到,只要Nacos裡面的配置不刪除,對應的流控規則持續有效。

10.3 步驟

10.3.1 POM
      <!--SpringCloud ailibaba sentinel-datasource-nacos -->
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-datasource-nacos</artifactId>
        </dependency>
10.3.2 YML
server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinel-service #8401微服務提供者後續將會被納入阿里巴巴sentinel監管
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848         #Nacos服務註冊中心地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制檯服務地址
                port: 8719 #預設8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
            web-context-unify: false # controller層的方法對service層呼叫不認為是同一個根鏈路
            datasource:
         ds1:
           nacos:
             server-addr: localhost:8848
             dataId: ${spring.application.name}
             groupId: DEFAULT_GROUP
             data-type: json
             rule-type: flow # com.alibaba.cloud.sentinel.datasource.RuleType

rule-type: flow

備註:rule-type是什麼?看看原始碼

com.alibaba.cloud.sentinel.datasource.RuleType

	/**
	 * flow.
	 */
	FLOW("flow", FlowRule.class),//流量
	/**
	 * degrade.
	 */
	DEGRADE("degrade", DegradeRule.class),//熔斷
	/**
	 * param flow.
	 */
	PARAM_FLOW("param-flow", ParamFlowRule.class),//訪問控制
	/**
	 * system.
	 */
	SYSTEM("system", SystemRule.class),//系統保護
	/**
	 * authority.
	 */
	AUTHORITY("authority", AuthorityRule.class),//熱點
10.3.3 Nacos業務規則配置

[
    {
        "resource": "/rateLimit/byUrl",
        "limitApp": "default",
        "grade": 1,
        "count": 1,
        "strategy": 0,
        "controlBehavior": 0,
        "clusterMode": false
    }
]

resource:資源名稱;limitApp:來源應用;grade:閾值型別,0表示執行緒數,1表示QPS;count:單機閾值;strategy:流控模式,0表示直接,1表示關聯,2表示鏈路;controlBehavior:流控效果,0表示快速失敗,1表示Warm Up,2表示排隊等待;clusterMode:是否叢集。

重啟,8401,呼叫http://localhost:8401/rateLimit/byUrl,還是有規則。

11.OpenFeign和Sentinel整合實現fallback降級

11.1 需求說明

cloudalibaba-consumer-nacos-order83 透過OpenFeign呼叫 cloudalibaba-provider-payment9001

  • 83 透過OpenFeign呼叫 9001微服務,正常訪問OK
  • 83 透過OpenFeign呼叫 9001微服務,異常訪問error
    • 訪問者要有fallback服務降級的情況,不要持續訪問9001加大微服務負擔,但是透過feign介面呼叫的又方法各自不同,如果每個不同方法都加一個fallback配對方法,會導致程式碼膨脹不好管理,工程埋雷....../(ㄒoㄒ)/~~
  • 3 public @interface FeignClient
    • 透過fallback屬性進行統一配置,feign介面裡面定義的全部方法都走統一的服務降級,一個搞定即可
  • 4 9001微服務自身還帶著sentinel內部配置的流控規則,如果滿足也會被觸發,也即本例有2個Case
    • 4.1 OpenFeign介面的統一fallback服務降級處理
    • 4.2 Sentinel訪問觸發了自定義的限流配置,在註解@SentinelResource裡面配置的blockHandler方法。

11.2 編碼步驟

啟動Nacos和Sentinel。

11.3 修改服務提供方

cloudalibaba-provider-payment9001

11.3.1 POM
 <!--openfeign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--alibaba-sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
11.3.2 YML
server:
  port: 9001

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848 #配置Nacos地址
    sentinel:
      transport:
        dashboard: localhost:8080 #配置Sentinel dashboard控制檯服務地址
        port: 8719 #預設8719埠,假如被佔用會自動從8719開始依次+1掃描,直至找到未被佔用的埠
11.3.3 主啟動

無改變。

11.3.4 業務類

PayAlibabaController

 	@GetMapping("/pay/nacos/get/{orderNo}")
    @SentinelResource(value = "getPayByOrderNo",blockHandler = "handlerBlockHandler")
    public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
    {
        //模擬從資料庫查詢出資料並賦值給DTO
        PayDTO payDTO = new PayDTO();

        payDTO.setId(1024);
        payDTO.setOrderNo(orderNo);
        payDTO.setAmount(BigDecimal.valueOf(9.9));
        payDTO.setPayNo("pay:"+ IdUtil.fastUUID());
        payDTO.setUserId(1);

        return ResultData.success("查詢返回值:"+payDTO);
    }
    public ResultData handlerBlockHandler(@PathVariable("orderNo") String orderNo, BlockException exception)
    {
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"getPayByOrderNo服務不可用," +
                "觸發sentinel流控配置規則"+"\t"+"o(╥﹏╥)o");
    }
    /*
    fallback服務降級方法納入到Feign介面統一處理,全域性一個
    public ResultData myFallBack(@PathVariable("orderNo") String orderNo,Throwable throwable)
    {
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"異常情況:"+throwable.getMessage());
    }
    */

啟動9001,自測,略。

11.4 修改cloud-api-commons

11.4.1 POM
    <!--openfeign-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    <!--alibaba-sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
11.4.2 新增PayFeignSentinelApi介面
package com.atguigu.cloud.apis;

import com.atguigu.cloud.resp.ResultData;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "nacos-payment-provider", fallback = PayFeignSentinelApiFallBack.class)
public interface PayFeignSentinelApi {
    @GetMapping("/pay/nacos/get/{orderNo}")
    ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo);
}
11.4.3 為遠端呼叫建立全域性統一服務降級類
package com.atguigu.cloud.apis;

import com.atguigu.cloud.resp.ResultData;
import com.atguigu.cloud.resp.ReturnCodeEnum;
import org.springframework.stereotype.Component;

@Component
public class PayFeignSentinelApiFallBack implements PayFeignSentinelApi{
    @Override
    public ResultData getPayByOrderNo(String orderNo) {
        return ResultData.fail(ReturnCodeEnum.RC500.getCode(),"對方服務當機或不可用,FallBack服務降級o(╥﹏╥)o");
    }
}

11.5 修改服務呼叫方

11.5.1 POM
        <!-- 引入自己定義的api通用包 -->
        <dependency>
            <groupId>com.atguigu.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--alibaba-sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
11.5.2 YML
server:
  port: 83

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
#消費者將要去訪問的微服務名稱(nacos微服務提供者叫什麼你寫什麼)
service-url:
  nacos-user-service: http://nacos-payment-provider

# 啟用Sentinel對Feign的支援
feign:
  sentinel:
    enabled: true
11.5.3 主啟動
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class Main83 {
    public static void main(String[] args) {
        SpringApplication.run(Main83.class, args);
    }
}
11.5.4 業務類
    @Resource
    private PayFeignSentinelApi payFeignSentinelApi;

    @GetMapping(value = "/consumer/pay/nacos/get/{orderNo}")
    public ResultData getPayByOrderNo(@PathVariable("orderNo") String orderNo)
    {
        return payFeignSentinelApi.getPayByOrderNo(orderNo);
    }
11.5.5 啟動

報錯:

2024-11-28T22:28:14.719+08:00 ERROR 15656 --- [nacos-order-consumer] [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'orderNacosController': Injection of resource dependencies failed

原因:SpringBoot + SpringCloud版本和Sentinel不相容。

解決方案:

父工程的POM修改

<!--        <spring.boot.version>3.2.0</spring.boot.version>-->
<!--        <spring.cloud.version>2023.0.0</spring.cloud.version>-->
        <spring.boot.version>3.0.9</spring.boot.version>
        <spring.cloud.version>2022.0.2</spring.cloud.version>

再次啟動成功。

11.6 測試

9001正常啟動、http://localhost:9001/pay/nacos/get/100
測試透過。

83正常啟動,http://localhost:83/consumer/pay/nacos/get/1024
,測試透過。

配置

http://localhost:83/consumer/pay/nacos/get/1024
頻繁訪問觸發流控規則,{"code":"500","message":"getPayByOrderNo服務不可用,觸發sentinel流控配置規則\to(╥﹏╥)o","data":null,"timestamp":1732804856992}。

關閉9001,測試服務降級效果。{"code":"500","message":"對方服務當機或不可用,FallBack服務降級o(╥﹏╥)o","data":null,"timestamp":1732804916203}

最後一步,恢復父工程版本號。

12.GateWay和Sentinel整合實現服務限流

12.1 需求說明

cloudalibaba-sentinel-gateway9528 保護 cloudalibaba-provider-payment9001

12.2 步驟

12.2.1 建Moudle

cloudalibaba-sentinel-gateway9528

12.2.2 改POM
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-transport-simple-http</artifactId>
            <version>1.8.6</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.csp</groupId>
            <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
            <version>1.8.6</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
12.2.3 寫YML
server:
  port: 9528

spring:
  application:
    name: cloudalibaba-sentinel-gateway     # sentinel+gataway整合Case
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      routes:
        - id: pay_routh1 #pay_routh1                #路由的ID(類似mysql主鍵ID),沒有固定規則但要求唯一,建議配合服務名
          uri: http://localhost:9001                #匹配後提供服務的路由地址
          predicates:
          - Path=/pay/**                      # 斷言,路徑相匹配的進行路由
12.2.4 主啟動
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class Main9528 {
    
    public static void main(String[] args) {
        SpringApplication.run(Main9528.class, args);
    }
    
}
12.2.5 業務類

參考官網配置案例改寫

https://github.com/alibaba/Sentinel/wiki/閘道器限流

import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.util.*;

@Configuration
public class GatewayConfiguration {
    private final List<ViewResolver> viewResolvers;
    private final ServerCodecConfigurer serverCodecConfigurer;

    public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                ServerCodecConfigurer serverCodecConfigurer) {
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        // Register the block exception handler for Spring Cloud Gateway.
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    @Bean
    @Order(-1)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

   @PostConstruct //javax.annotation.PostConstruct
    public void doInit() {
        initBlockHandler();
    }


    //處理/自定義返回的例外資訊
    private void initBlockHandler() {
        Set<GatewayFlowRule> rules = new HashSet<>();
        rules.add(new GatewayFlowRule("pay_routh1").setCount(2).setIntervalSec(1));

        GatewayRuleManager.loadRules(rules);
        BlockRequestHandler handler = new BlockRequestHandler() {
            @Override
            public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
                Map<String,String> map = new HashMap<>();

                map.put("errorCode", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
                map.put("errorMessage", "請求太過頻繁,系統忙不過來,觸發限流(sentinel+gataway整合Case)");

                return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                        .contentType(MediaType.APPLICATION_JSON)
                        .body(BodyInserters.fromValue(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(handler);
    }
}

12.3 測試

9001訪問:http://localhost:9001/pay/nacos/get/100
,加快點選頻率,不會出現限流容錯。

加閘道器訪問:http://localhost:9528/pay/nacos/get/100
,加快點選頻率,會出現限流容錯。{"errorMessage":"請求太過頻繁,系統忙不過來,觸發限流(sentinel+gataway整合Case)","errorCode":"Too Many Requests"}

只是為了記錄自己的學習歷程,且本人水平有限,不對之處,請指正。

相關文章