sentinel (史上最全+入門教程)


文章很長,建議收藏起來慢慢讀!瘋狂創客圈總目錄 語雀版 | 總目錄 碼雲版| 總目錄 博客園版 為您奉上珍貴的學習資源 :


推薦:入大廠 、做架構、大力提升Java 內功 的 精彩博文

入大廠 、做架構、大力提升Java 內功 必備的精彩博文 2021 秋招漲薪1W + 必備的精彩博文
1:Redis 分布式鎖 (圖解-秒懂-史上最全) 2:Zookeeper 分布式鎖 (圖解-秒懂-史上最全)
3: Redis與MySQL雙寫一致性如何保證? (面試必備) 4: 面試必備:秒殺超賣 解決方案 (史上最全)
5:面試必備之:Reactor模式 6: 10分鍾看懂, Java NIO 底層原理
7:TCP/IP(圖解+秒懂+史上最全) 8:Feign原理 (圖解)
9:DNS圖解(秒懂 + 史上最全 + 高薪必備) 10:CDN圖解(秒懂 + 史上最全 + 高薪必備)
11: 分布式事務( 圖解 + 史上最全 + 吐血推薦 ) 12:限流:計數器、漏桶、令牌桶
三大算法的原理與實戰(圖解+史上最全)
13:架構必看:12306搶票系統億級流量架構
(圖解+秒懂+史上最全)
14:seata AT模式實戰(圖解+秒懂+史上最全)
15:seata 源碼解讀(圖解+秒懂+史上最全) 16:seata TCC模式實戰(圖解+秒懂+史上最全)

Java 面試題 30個專題 , 史上最全 , 面試必刷 阿里、京東、美團... 隨意挑、橫着走!!!
1: JVM面試題(史上最強、持續更新、吐血推薦) 2:Java基礎面試題(史上最全、持續更新、吐血推薦
3:架構設計面試題 (史上最全、持續更新、吐血推薦) 4:設計模式面試題 (史上最全、持續更新、吐血推薦)
17、分布式事務面試題 (史上最全、持續更新、吐血推薦) 一致性協議 (史上最全)
29、多線程面試題(史上最全) 30、HR面經,過五關斬六將后,小心陰溝翻船!
9.網絡協議面試題(史上最全、持續更新、吐血推薦) 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄

SpringCloud 微服務 精彩博文
nacos 實戰(史上最全) sentinel (史上最全+入門教程)
SpringCloud gateway (史上最全) 更多專題, 請參見【 瘋狂創客圈 高並發 總目錄

sentinel 基本概念

開發的原因,需要對吞吐量(TPS)、QPS、並發數、響應時間(RT)幾個概念做下了解,查自百度百科,記錄如下:

  1. 響應時間(RT)
      響應時間是指系統對請求作出響應的時間。直觀上看,這個指標與人對軟件性能的主觀感受是非常一致的,因為它完整地記錄了整個計算機系統處理請求的時間。由於一個系統通常會提供許多功能,而不同功能的處理邏輯也千差萬別,因而不同功能的響應時間也不盡相同,甚至同一功能在不同輸入數據的情況下響應時間也不相同。所以,在討論一個系統的響應時間時,人們通常是指該系統所有功能的平均時間或者所有功能的最大響應時間。當然,往往也需要對每個或每組功能討論其平均響應時間和最大響應時間。
      對於單機的沒有並發操作的應用系統而言,人們普遍認為響應時間是一個合理且准確的性能指標。需要指出的是,響應時間的絕對值並不能直接反映軟件的性能的高低,軟件性能的高低實際上取決於用戶對該響應時間的接受程度。對於一個游戲軟件來說,響應時間小於100毫秒應該是不錯的,響應時間在1秒左右可能屬於勉強可以接受,如果響應時間達到3秒就完全難以接受了。而對於編譯系統來說,完整編譯一個較大規模軟件的源代碼可能需要幾十分鍾甚至更長時間,但這些響應時間對於用戶來說都是可以接受的。
  2. 吞吐量(Throughput)
    吞吐量是指系統在單位時間內處理請求的數量。對於無並發的應用系統而言,吞吐量與響應時間成嚴格的反比關系,實際上此時吞吐量就是響應時間的倒數。前面已經說過,對於單用戶的系統,響應時間(或者系統響應時間和應用延遲時間)可以很好地度量系統的性能,但對於並發系統,通常需要用吞吐量作為性能指標。
      對於一個多用戶的系統,如果只有一個用戶使用時系統的平均響應時間是t,當有你n個用戶使用時,每個用戶看到的響應時間通常並不是n×t,而往往比n×t小很多(當然,在某些特殊情況下也可能比n×t大,甚至大很多)。這是因為處理每個請求需要用到很多資源,由於每個請求的處理過程中有許多不走難以並發執行,這導致在具體的一個時間點,所占資源往往並不多。也就是說在處理單個請求時,在每個時間點都可能有許多資源被閑置,當處理多個請求時,如果資源配置合理,每個用戶看到的平均響應時間並不隨用戶數的增加而線性增加。實際上,不同系統的平均響應時間隨用戶數增加而增長的速度也不大相同,這也是采用吞吐量來度量並發系統的性能的主要原因。一般而言,吞吐量是一個比較通用的指標,兩個具有不同用戶數和用戶使用模式的系統,如果其最大吞吐量基本一致,則可以判斷兩個系統的處理能力基本一致。
  3. 並發用戶數
    並發用戶數是指系統可以同時承載的正常使用系統功能的用戶的數量。與吞吐量相比,並發用戶數是一個更直觀但也更籠統的性能指標。實際上,並發用戶數是一個非常不准確的指標,因為用戶不同的使用模式會導致不同用戶在單位時間發出不同數量的請求。一網站系統為例,假設用戶只有注冊后才能使用,但注冊用戶並不是每時每刻都在使用該網站,因此具體一個時刻只有部分注冊用戶同時在線,在線用戶就在瀏覽網站時會花很多時間閱讀網站上的信息,因而具體一個時刻只有部分在線用戶同時向系統發出請求。這樣,對於網站系統我們會有三個關於用戶數的統計數字:注冊用戶數、在線用戶數和同時發請求用戶數。由於注冊用戶可能長時間不登陸網站,使用注冊用戶數作為性能指標會造成很大的誤差。而在線用戶數和同事發請求用戶數都可以作為性能指標。相比而言,以在線用戶作為性能指標更直觀些,而以同時發請求用戶數作為性能指標更准確些。
  4. QPS每秒查詢率(Query Per Second)
    每秒查詢率QPS是對一個特定的查詢服務器在規定時間內所處理流量多少的衡量標准,在因特網上,作為域名系統服務器的機器的性能經常用每秒查詢率來衡量。對應fetches/sec,即每秒的響應請求數,也即是最大吞吐能力。 (看來是類似於TPS,只是應用於特定場景的吞吐量)

1、什么是Sentinel:

Sentinel是阿里開源的項目,提供了流量控制、熔斷降級、系統負載保護等多個維度來保障服務之間的穩定性。
官網:https://github.com/alibaba/Sentinel/wiki

2012年,Sentinel誕生於阿里巴巴,其主要目標是流量控制。2013-2017年,Sentinel迅速發展,並成為阿里巴巴所有微服務的基本組成部分。 它已在6000多個應用程序中使用,涵蓋了幾乎所有核心電子商務場景。2018年,Sentinel演變為一個開源項目。2020年,Sentinel Golang發布。

Sentinel 具有以下特征:

豐富的應用場景 :Sentinel 承接了阿里巴巴近 10 年的雙十一大促流量的核心場景,例如秒殺(即
突發流量控制在系統容量可以承受的范圍)、消息削峰填谷、集群流量控制、實時熔斷下游不可用應用等。
完備的實時監控 :Sentinel 同時提供實時的監控功能。您可以在控制台中看到接入應用的單台機
器秒級數據,甚至 500 台以下規模的集群的匯總運行情況。
廣泛的開源生態 :Sentinel 提供開箱即用的與其它開源框架/庫的整合模塊,例如與 Spring
Cloud、Dubbo、gRPC 的整合。您只需要引入相應的依賴並進行簡單的配置即可快速地接入Sentinel。

完善的 SPI 擴展點:Sentinel 提供簡單易用、完善的 SPI 擴展接口。您可以通過實現擴展接口來快
速地定制邏輯。例如定制規則管理、適配動態數據源等。

Sentinel的生態圈

img

Sentinel主要特性:

img

關於Sentinel與Hystrix的區別見:https://yq.aliyun.com/articles/633786/

到這已經學習Sentinel的基本的使用,在很多的特性和Hystrix有很多類似的功能。以下是Sentinel和Hystrix的對比。

Sentinel 的使用

Sentinel 的使用可以分為兩個部分:

  • 控制台(Dashboard):控制台主要負責管理推送規則、監控、集群限流分配管理、機器發現等。

  • 核心庫(Java 客戶端):不依賴任何框架/庫,能夠運行於 Java 7 及以上的版本的運行時環境,同時對 Dubbo / Spring Cloud 等框架也有較好的支持。

在這里我們看下控制台的使用

Sentinel中的管理控制台

2.1 獲取 Sentinel 控制台

您可以從 release 頁面 下載最新版本的控制台 jar 包。

您也可以從最新版本的源碼自行構建 Sentinel 控制台:

  • 下載 控制台 工程
  • 使用以下命令將代碼打包成一個 fat jar: mvn clean package

2.2 sentinel服務啟動

java  -server -Xms64m -Xmx256m  -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar 


開機啟動:啟動命令可以加入到啟動的 rc.local 配置文件, 之后做到開機啟動

啟動 sentinel

/usr/bin/su - root -c "nohup java -server -Xms64m -Xmx256m -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar 2>&1 &"

除了流量控制以外,對調用鏈路中不穩定的資源進行熔斷降級也是保障高可用的重要措施之一。

由於調用關系的復雜性,如果調用鏈路中的某個資源不穩定,最終會導致請求發生堆積。Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)。

關於熔斷降級的介紹見:Sentinel熔斷降級。

下面就使用基於注解的方式實現Sentinel的熔斷降級的demo。

注意:啟動 Sentinel 控制台需要 JDK 版本為 1.8 及以上版本。

使用如下命令啟動控制台:

nohup java  -server -Xms64m -Xmx256m  -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.7.1.jar &

其中 -Dserver.port=8849用於指定 Sentinel 控制台端口為 8849 , 這個端口可以按需指定。

從 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登錄功能,默認用戶名和密碼都是 sentinel。可以參考 鑒權模塊文檔 配置用戶名和密碼。

注:若您的應用為 Spring Boot 或 Spring Cloud 應用,您可以通過 Spring 配置文件來指定配置,詳情請參考 Spring Cloud Alibaba Sentinel 文檔。(1)獲取 Sentinel 控制台
您可以從官方 網站中 下載最新版本的控制台 jar 包,下載地址如下:

https://github.com/alibaba/Sentinel/releases/download/1.6.3/sentinel-dashboard-1.7.1.jar

(2)啟動
使用如下命令啟動控制台:
其中 - Dserver.port=8888 用於指定 Sentinel 控制台端口為 8888 。
從 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登錄功能,默認用戶名和密碼都是 sentinel 。可以參考 鑒權模塊文檔 配置用戶名和密碼。

[root@192 ~]# java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.6.3.jar
INFO: log base dir is: /root/logs/csp/
INFO: log name use pid is: false

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2020-02-08 13:07:29.316  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : Starting DashboardApplication on 192.168.180.137 with PID 114031 (/root/sentinel-dashboard-1.6.3.jar started by root in /root)
2020-02-08 13:07:29.319  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : No active profile set, falling back to default profiles: default
2020-02-08 13:07:29.456  INFO 114031 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@59690aa4: startup date [Sat Feb 08 13:07:29 CST 2020]; root of context hierarchy
2020-02-08 13:07:33.783  INFO 114031 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8888 (http)

啟動 Sentinel 控制台需要 JDK 版本為 1.8 及以上版本。

img

查看機器列表以及健康情況
默認情況下Sentinel 會在客戶端首次調用的時候進行初始化,開始向控制台發送心跳包。也可以配置
sentinel.eager=true ,取消Sentinel控制台懶加載。
打開瀏覽器即可展示Sentinel的管理控制台

img

客戶端能接入控制台

控制台啟動后,客戶端需要按照以下步驟接入到控制台。

父工程引入 alibaba實現的SpringCloud

<dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Greenwich.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

子工程中引入 sentinel

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

( 2)配置啟動參數
在工程的application.yml中添加Sentinel 控制台配置信息

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.180.137:8888   #sentinel控制台的請求地址

這里的 spring.cloud.sentinel.transport.dashboard 配置控制台的請求路徑。

Sentinel與Hystrix的區別

image

遷移方案
Sentinel 官方提供了詳細的由Hystrix 遷移到Sentinel 的方法

img

2 使用 Sentinel 來進行熔斷與限流

Sentinel 可以簡單的分為 Sentinel 核心庫和 Dashboard。核心庫不依賴 Dashboard,但是結合
Dashboard 可以取得最好的效果。
使用 Sentinel 來進行熔斷保護,主要分為幾個步驟:

  1. 定義資源

    資源:可以是任何東西,一個服務,服務里的方法,甚至是一段代碼。

  2. 定義規則

    規則:Sentinel 支持以下幾種規則:流量控制規則、熔斷降級規則、系統保護規則、來源訪問控制規則
    和 熱點參數規則。

  3. 檢驗規則是否生效

Sentinel 的所有規則都可以在內存態中動態地查詢及修改,修改之后立即生效. 先把可能需要保護的資源定義好,之后再配置規則。

也可以理解為,只要有了資源,我們就可以在任何時候靈活地定義各種流量控制規則。在編碼的時候,只需要考慮這個代碼是否需要保護,如果需要保
護,就將之定義為一個資源。

1. 定義資源

資源是 Sentinel 的關鍵概念。它可以是 Java 應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它應用提供的服務,RPC接口方法,甚至可以是一段代碼。

只要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,甚至服務名稱作為資源名來標示資源。

把需要控制流量的代碼用 Sentinel的關鍵代碼 SphU.entry("資源名") 和 entry.exit() 包圍起來即可。

實例代碼:

    Entry entry = null;
    try {
        // 定義一個sentinel保護的資源,名稱為test-sentinel-api
        entry = SphU.entry(resourceName);
        // 模擬執行被保護的業務邏輯耗時
        Thread.sleep(100);
        return a;
    } catch (BlockException e) {
        // 如果被保護的資源被限流或者降級了,就會拋出BlockException
        log.warn("資源被限流或降級了", e);
        return "資源被限流或降級了";
    } catch (InterruptedException e) {
        return "發生InterruptedException";
    } finally {
        if (entry != null) {
            entry.exit();
        }
 
        ContextUtil.exit();
    }
}

在下面的例子中, 用 try-with-resources 來定義資源。參考代碼如下:

public static void main(String[] args) {
    // 配置規則.
    initFlowRules();

    while (true) {
        // 1.5.0 版本開始可以直接利用 try-with-resources 特性
        try (Entry entry = SphU.entry("HelloWorld")) {
            // 被保護的邏輯
            System.out.println("hello world");
	} catch (BlockException ex) {
            // 處理被流控的邏輯
	    System.out.println("blocked!");
	}
    }
}

資源注解@SentinelResource

也可以使用Sentinel提供的注解@SentinelResource來定義資源,實例如下:

@SentinelResource("HelloWorld")
public void helloWorld() {
    // 資源中的邏輯
    System.out.println("hello world");
}

@SentinelResource 注解

注意:注解方式埋點不支持 private 方法。

@SentinelResource 用於定義資源,並提供可選的異常處理和 fallback 配置項。 @SentinelResource 注解包含以下屬性:

  • value:資源名稱,必需項(不能為空)
  • entryType:entry 類型,可選項(默認為 EntryType.OUT)
  • blockHandler / blockHandlerClass:

blockHandler 對應處理 BlockException的函數名稱,可選項。blockHandler 函數訪問范圍需要是 public,返回類型需要與原方法相匹配,參數類型需要和原方法相匹配並且最后加一個額外的參數,類型為 BlockException。blockHandler 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 blockHandlerClass 為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。

  • fallback /fallbackClass

    :fallback 函數名稱,可選項,用於在拋出異常的時候提供 fallback 處理邏輯。fallback 函數可以針對所有類型的異常(除了exceptionsToIgnore里面排除掉的異常類型)進行處理。

  • defaultFallback

    (since 1.6.0):默認的 fallback 函數名稱,可選項,通常用於通用的 fallback 邏輯(即可以用於很多服務或方法)。默認 fallback 函數可以針對所有類型的異常(除了exceptionsToIgnore里面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,則只有 fallback 會生效。

fallback 函數簽名和位置要求:

  • 返回值類型必須與原函數返回值類型一致;

  • 方法參數列表需要和原函數一致,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。

  • fallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。

defaultFallback 函數簽名要求:

  • 返回值類型必須與原函數返回值類型一致;
  • 方法參數列表需要為空,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。
  • defaultFallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 為對應的類的 Class 對象,注意對應的函數必需為 static 函數,否則無法解析。
  • exceptionsToIgnore(since 1.6.0):用於指定哪些異常被排除掉,不會計入異常統計中,也不會進入 fallback 邏輯中,而是會原樣拋出。

2. 定義規則

規則主要有流控規則、 熔斷降級規則、系統規則、權限規則、熱點參數規則等:

一段硬編碼的方式定義流量控制規則如下:

private void initSystemRule() {
    List<SystemRule> rules = new ArrayList<>();
    SystemRule rule = new SystemRule();
    rule.setHighestSystemLoad(10);
    rules.add(rule);
    SystemRuleManager.loadRules(rules);
}

加載規則:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控規則
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降級規則
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系統規則
AuthorityRuleManager.loadRules(List<AuthorityRule> rules); // 修改授權規則

3 sentinel 熔斷降級

1 什么是熔斷降級

熔斷降級對調用鏈路中不穩定的資源進行熔斷降級是保障高可用的重要措施之一。

由於調用關系的復雜性,如果調用鏈路中的某個資源不穩定,最終會導致請求發生堆積。Sentinel 熔斷降級會在調用鏈路中某個資源出現不穩定狀態時(例如調用超時或異常比例升高),對這個資源的調用進行限制,讓請求快速失敗,避免影響到其它的資源而導致級聯錯誤。當資源被降級后,在接下來的降級時間窗口之內,對該資源的調用都自動熔斷(默認行為是拋出 DegradeException)

2. 熔斷降級規則

熔斷降級規則包含下面幾個重要的屬性:

Field 說明 默認值
resource 資源名,即規則的作用對象
grade 熔斷策略,支持慢調用比例/異常比例/異常數策略 慢調用比例
count 慢調用比例模式下為慢調用臨界 RT(超出該值計為慢調用);異常比例/異常數模式下為對應的閾值
timeWindow 熔斷時長,單位為 s
minRequestAmount 熔斷觸發的最小請求數,請求數小於該值時即使異常比率超出閾值也不會熔斷(1.7.0 引入) 5
statIntervalMs 統計時長(單位為 ms),如 60*1000 代表分鍾級(1.8.0 引入) 1000 ms
slowRatioThreshold 慢調用比例閾值,僅慢調用比例模式有效(1.8.0 引入)

3 幾種降級策略

我們通常用以下幾種降級策略:

  • 平均響應時間 (DEGRADE_GRADE_RT):

    當資源的平均響應時間超過閾值(DegradeRule 中的 count,以 ms 為單位)之后,資源進入准降級狀態。如果接下來 1s 內持續進入 5 個請求(即 QPS >= 5),它們的 RT 都持續超過這個閾值,那么在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地熔斷(拋出 DegradeException)。

    注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項 -Dcsp.sentinel.statistic.max.rt=xxx 來配置。

  • 異常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):

    當資源的每秒異常總數占通過量的比值超過閾值(DegradeRule 中的 count)之后,資源進入降級狀態,即在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地返回。

    異常比率的閾值范圍是 [0.0, 1.0],代表 0% - 100%。

  • 異常數 (DEGRADE_GRADE_EXCEPTION_COUNT):

    當資源近 1 分鍾的異常數目超過閾值之后會進行熔斷。

    注意由於統計時間窗口是分鍾級別的,若 timeWindow 小於 60s,則結束熔斷狀態后仍可能再進入熔斷狀態。

4 熔斷降級代碼實現

可以通過調用 DegradeRuleManager.loadRules() 方法來用硬編碼的方式定義流量控制規則。

    @PostConstruct
    public void initSentinelRule()
    {
        //熔斷規則: 5s內調用接口出現異常次數超過5的時候, 進行熔斷
        List<DegradeRule> degradeRules = new ArrayList<>();
        DegradeRule rule = new DegradeRule();
        rule.setResource("queryGoodsInfo");
        rule.setCount(5);

        rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);//熔斷規則
        rule.setTimeWindow(5);
        degradeRules.add(rule);
        DegradeRuleManager.loadRules(degradeRules);
    }

具體源碼,請參見瘋狂創客圈crazy-springcloud 源碼工程

5 控制台降級規則

配置

在這里插入圖片描述

參數

Field 說明 默認值
resource 資源名,即限流規則的作用對象
count 閾值
grade 降級模式,根據 RT 降級還是根據異常比例降級 RT
timeWindow 降級的時間,單位為 s

6 與Hystrix的熔斷對比:

Hystrix常用的線程池隔離會造成線程上下切換的overhead比較大;Hystrix使用的信號量隔離對某個資源調用的並發數進行控制,效果不錯,但是無法對慢調用進行自動降級;

Sentinel通過並發線程數的流量控制提供信號量隔離的功能;此外,Sentinel支持的熔斷降級維度更多,可對多種指標進行流控、熔斷,且提供了實時監控和控制面板,功能更為強大。

4 Sentinel 流控(限流)

流量控制(Flow Control),原理是監控應用流量的QPS或並發線程數等指標,當達到指定閾值時對流量進行控制,避免系統被瞬時的流量高峰沖垮,保障應用高可用性。

通過流控規則來指定允許該資源通過的請求次數,例如下面的代碼定義了資源 HelloWorld 每秒最多只能通過 20 個請求。 參考的規則定義如下:

private static void initFlowRules(){
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);
}

一條限流規則主要由下面幾個因素組成,我們可以組合這些元素來實現不同的限流效果:

  • resource:資源名,即限流規則的作用對象

  • count: 限流閾值

  • grade: 限流閾值類型(QPS 或並發線程數)

  • limitApp: 流控針對的調用來源,若為 default 則不區分調用來源

  • strategy: 調用關系限流策略

  • controlBehavior: 流量控制效果(直接拒絕、Warm Up、勻速排隊)

基本的參數

資源名:唯一名稱,默認請求路徑

針對來源:Sentinel可以針對調用者進行限流,填寫微服務名,默認為default(不區分來源)

閾值類型/單機閾值:

1.QPS:每秒請求數,當前調用該api的QPS到達閾值的時候進行限流

2.線程數:當調用該api的線程數到達閾值的時候,進行限流

是否集群:是否為集群

流控的幾種strategy

1.直接:當api大達到限流條件時,直接限流

2.關聯:當關聯的資源到達閾值,就限流自己

3.鏈路:只記錄指定路上的流量,指定資源從入口資源進來的流量,如果達到閾值,就進行限流,api級別的限流

4.1 直接失敗模式

使用API進行資源定義


    /**
     * 限流實現方式一: 拋出異常的方式定義資源
     *
     * @param orderId
     * @return
     */
    @ApiOperation(value = "純代碼限流")
    @GetMapping("/getOrder")
    @ResponseBody
    public String getOrder(@RequestParam(value = "orderId", required = false)String orderId)
    {

        Entry entry = null;
        // 資源名
        String resourceName = "getOrder";
        try
        {
            // entry可以理解成入口登記
            entry = SphU.entry(resourceName);
            // 被保護的邏輯, 這里為訂單查詢接口
            return "正常的業務邏輯 OrderInfo :" + orderId;
        } catch (BlockException blockException)
        {
            // 接口被限流的時候, 會進入到這里
            log.warn("---getOrder1接口被限流了---, exception: ", blockException);
            return "接口限流, 返回空";
        } finally
        {
            // SphU.entry(xxx) 需要與 entry.exit() 成對出現,否則會導致調用鏈記錄異常
            if (entry != null)
            {
                entry.exit();
            }
        }

    }

代碼限流規則

//限流規則 QPS mode,
        List<FlowRule> rules = new ArrayList<FlowRule>();
        FlowRule rule1 = new FlowRule();
        rule1.setResource("getOrder");
        // QPS控制在2以內
        rule1.setCount(2);
        // QPS限流
        rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
        rule1.setLimitApp("default");
        rules.add(rule1);
        FlowRuleManager.loadRules(rules);

網頁限流規則配置

選擇QPS,直接,快速失敗,單機閾值為2。

配置

![流控規則](在這里插入圖片描述

參數

Field 說明 默認值
resource 資源名,資源名是限流規則的作用對象
count 限流閾值
grade 限流閾值類型,QPS 或線程數模式 QPS 模式
limitApp 流控針對的調用來源 default,代表不區分調用來源
strategy 判斷的根據是資源自身,還是根據其它關聯資源 (refResource),還是根據鏈路入口 根據資源本身
controlBehavior 流控效果(直接拒絕 / 排隊等待 / 慢啟動模式) 直接拒絕

測試

頻繁刷新請求,1秒訪問2次請求,正常,超過設置的閾值,將報默認的錯誤。

img

再次的1秒訪問2次請求,訪問正常。超過2次,訪問異常

4.2 關聯模式

調用關系包括調用方、被調用方;一個方法又可能會調用其它方法,形成一個調用鏈路的層次關系。Sentinel 通過 NodeSelectorSlot 建立不同資源間的調用的關系,並且通過 ClusterBuilderSlot 記錄每個資源的實時統計信息。

當兩個資源之間具有資源爭搶或者依賴關系的時候,這兩個資源便具有了關聯。

比如對數據庫同一個字段的讀操作和寫操作存在爭搶,讀的速度過高會影響寫得速度,寫的速度過高會影響讀的速度。如果放任讀寫操作爭搶資源,則爭搶本身帶來的開銷會降低整體的吞吐量。可使用關聯限流來避免具有關聯關系的資源之間過度的爭搶.

舉例來說,read_dbwrite_db 這兩個資源分別代表數據庫讀寫,我們可以給 read_db 設置限流規則來達到寫優先的目的。具體的方法:

設置 `strategy` 為 `RuleConstant.STRATEGY_RELATE` 
設置 `refResource` 為 `write_db`。
這樣當寫庫操作過於頻繁時,讀數據的請求會被限流。

還有一個例子,電商的 下訂單 和 支付兩個操作,需要優先保障 支付, 可以根據 支付接口的 流量閾值,來對訂單接口進行限制,從而保護支付的目的。

使用注解進行資源定義

添加2個請求

   @SentinelResource(value = "test1", blockHandler = "exceptionHandler")
    @GetMapping("/test1")
    public String test1()
    {
        log.info(Thread.currentThread().getName() + "\t" + "...test1");
        return "-------hello baby,i am test1";
    }


    // Block 異常處理函數,參數最后多一個 BlockException,其余與原函數一致.
    public String exceptionHandler(BlockException ex)
    {
        // Do some log here.
        ex.printStackTrace();
        log.info(Thread.currentThread().getName() + "\t" + "...exceptionHandler");
        return String.format("error: test1  is not OK");
    }

    @SentinelResource(value = "test1_ref")
    @GetMapping("/test1_ref")
    public String test1_ref()
    {
        log.info(Thread.currentThread().getName() + "\t" + "...test1_related");
        return "-------hello baby,i am test1_ref";
    }

代碼配置關聯限流規則


        // 關聯模式流控  QPS控制在1以內
        String refResource = "test1_ref";
        FlowRule rRule = new FlowRule("test1")
                .setCount(1)  // QPS控制在1以內
                .setStrategy(RuleConstant.STRATEGY_RELATE)
                .setRefResource(refResource);

        rules.add(rRule);
        FlowRuleManager.loadRules(rules);

網頁限流規則配置

在這里插入圖片描述

測試

選擇QPS,單機閾值為1,選擇關聯,關聯資源為/test_ref,這里用Jmeter模擬高並發,請求/test_ref。

img

在大批量線程高並發訪問/test_ref,導致/test失效了

img

鏈路類型的關聯也類似,就不再演示了。多個請求調用同一微服務。

4.3 Warm up(預熱)模式

當流量突然增大的時候,我們常常會希望系統從空閑狀態到繁忙狀態的切換的時間長一些。即如果系統在此之前長期處於空閑的狀態,我們希望處理請求的數量是緩步的增多,經過預期的時間以后,到達系統處理請求個數的最大值。Warm Up(冷啟動,預熱)模式就是為了實現這個目的的。

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

使用注解定義資源

    @SentinelResource(value = "testWarmUP", blockHandler = "exceptionHandlerOfWarmUp")
    @GetMapping("/testWarmUP")
    public String testWarmUP()
    {
        log.info(Thread.currentThread().getName() + "\t" + "...test1");
        return "-------hello baby,i am testWarmUP";
    }

代碼限流規則


        FlowRule warmUPRule = new FlowRule();
        warmUPRule.setResource("testWarmUP");
        warmUPRule.setCount(20);
        warmUPRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        warmUPRule.setLimitApp("default");
        warmUPRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
        warmUPRule.setWarmUpPeriodSec(10);

網頁限流規則配置

在這里插入圖片描述

先在單機閾值10/3,3的時候,預熱10秒后,慢慢將閾值升至20。剛開始刷/testWarmUP,會出現默認錯誤,預熱時間到了后,閾值增加,沒超過閾值刷新,請求正常。

通常冷啟動的過程系統允許通過的 QPS 曲線如下圖所示:

img

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

通過jmeter進行測試

4.4 排隊等待模式

勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式會嚴格控制請求通過的間隔時間,也即是讓請求以均勻的速度通過,對應的是漏桶算法。閾值必須設置為QPS。

img

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

某瞬時來了大流量的請求, 而如果此時要處理所有請求,很可能會導致系統負載過高,影響穩定性。但其實可能后面幾秒之內都沒有消息投遞,若直接把多余的消息丟掉則沒有充分利用系統處理消息的能力。Sentinel的Rate Limiter模式能在某一段時間間隔內以勻速方式處理這樣的請求, 充分利用系統的處理能力, 也就是削峰填谷, 保證資源的穩定性.

Sentinel會以固定的間隔時間讓請求通過, 訪問資源。當請求到來的時候,如果當前請求距離上個通過的請求通過的時間間隔不小於預設值,則讓當前請求通過;否則,計算當前請求的預期通過時間,如果該請求的預期通過時間小於規則預設的 timeout 時間,則該請求會等待直到預設時間到來通過;反之,則馬上拋出阻塞異常。

使用Sentinel的這種策略, 簡單點說, 就是使用一個時間段(比如20s的時間)處理某一瞬時產生的大量請求, 起到一個削峰填谷的作用, 從而充分利用系統的處理能力, 下圖能很形象的展示這種場景: X軸代表時間, Y軸代表系統處理的請求.

img

示例

模擬2個用戶同時並發的訪問資源,發出100個請求,

如果設置QPS閾值為1, 拒絕策略修改為Rate Limiter勻速RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER方式, 還需要設置setMaxQueueingTimeMs(20 * 1000)表示每一請求最長等待時間, 這里等待時間大一點, 以保證讓所有請求都能正常通過;

假設這里設置的排隊等待時間過小的話, 導致排隊等待的請求超時而拋出異常BlockException, 最終結果可能是這100個並發請求中只有一個請求或幾個才能正常通過, 所以使用這種模式得根據訪問資源的耗時時間決定排隊等待時間. 按照目前這種設置, QPS閾值為10的話, 每一個請求相當於是以勻速100ms左右通過.

使用注解定義資源

  
    
    @SentinelResource(value = "testLineUp",
    					blockHandler = "exceptionHandlerOftestLineUp")
    @GetMapping("/testLineUp")
        public String testLineUp()
    {
        log.info(Thread.currentThread().getName() + "\t" + "...test1");
        return "-------hello baby,i am testLineUp";
    }

代碼限流規則

    
        FlowRule lineUpRule = new FlowRule();
        lineUpRule.setResource("testLineUp");
        lineUpRule.setCount(10);
        lineUpRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        lineUpRule.setLimitApp("default");
        lineUpRule.setMaxQueueingTimeMs(20 * 1000);
        // CONTROL_BEHAVIOR_DEFAULT means requests more than threshold will be rejected immediately.
        // CONTROL_BEHAVIOR_DEFAULT將超過閾值的流量立即拒絕掉.
        lineUpRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
        rules.add(lineUpRule);

網頁限流規則配置

在這里插入圖片描述

通過jmeter進行測試

在這里插入圖片描述

4.5 熱點規則 (ParamFlowRule)

何為熱點?熱點即經常訪問的數據。很多時候我們希望統計某個熱點數據中訪問頻次最高的 Top K 數據,並對其訪問進行限制。比如:

  • 商品 ID 為參數,統計一段時間內最常購買的商品 ID 並進行限制
  • 用戶 ID 為參數,針對一段時間內頻繁訪問的用戶 ID 進行限制 熱點參數限流會統計傳入參數中的熱點參數,並根據配置的限流閾值與模式,對包含熱點參數的資源調用進行限流。熱點參數限流可以看做是一種特殊的流量控制,僅對包含熱點參數的資源調用生效。 使用該規則需要引入依賴:

熱點參數規則(ParamFlowRule)類似於流量控制規則(FlowRule):

屬性 說明 默認值
resource 資源名,必填
count 限流閾值,必填
grade 限流模式 QPS 模式
durationInSec 統計窗口時間長度(單位為秒),1.6.0 版本開始支持 1s
controlBehavior 流控效果(支持快速失敗和勻速排隊模式),1.6.0 版本開始支持 快速失敗
maxQueueingTimeMs 最大排隊等待時長(僅在勻速排隊模式生效),1.6.0 版本開始支持 0ms
paramIdx 熱點參數的索引,必填,對應 SphU.entry(xxx, args) 中的參數索引位置
paramFlowItemList 參數例外項,可以針對指定的參數值單獨設置限流閾值,不受前面 count 閾值的限制。僅支持基本類型和字符串類型
clusterMode 是否是集群參數流控規則 false
clusterConfig 集群流控相關配置

自定義資源


    @GetMapping("/byHotKey")
    @SentinelResource(value = "byHotKey",
            blockHandler = "userAccessError")
    public String test4(@RequestParam(value = "userId", required = false) String userId,
                        @RequestParam(value = "goodId", required = false) int goodId)
    {
        log.info(Thread.currentThread().getName() + "\t" + "...byHotKey");
        return "-----------by HotKey: UserId";
    }

限流規則代碼:

可以通過 ParamFlowRuleManager 的 loadRules 方法更新熱點參數規則,下面是官方實例:

ParamFlowRule rule = new ParamFlowRule(resourceName)
    .setParamIdx(0)
    .setCount(5);
// 針對 int 類型的參數 PARAM_B,單獨設置限流 QPS 閾值為 10,而不是全局的閾值 5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
    .setClassType(int.class.getName())
    .setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));

ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

具體的限流代碼如下:

        ParamFlowRule pRule = new ParamFlowRule("byHotKey")
                .setParamIdx(1)
                .setCount(1);
// 針對 參數值1000,單獨設置限流 QPS 閾值為 5,而不是全局的閾值 1.
        ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(1000))
                .setClassType(int.class.getName())
                .setCount(5);
        pRule.setParamFlowItemList(Collections.singletonList(item));

        ParamFlowRuleManager.loadRules(Collections.singletonList(pRule));

網頁限流規則配置

在這里插入圖片描述

測試:

請參見視頻

5. Sentinel 系統保護

系統保護的目的

在開始之前,我們先了解一下系統保護的目的:

  • 保證系統不被拖垮
  • 在系統穩定的前提下,保持系統的吞吐量

長期以來,系統保護的思路是根據硬指標,即系統的負載 (load1) 來做系統過載保護。當系統負載高於某個閾值,就禁止或者減少流量的進入;當 load 開始好轉,則恢復流量的進入。這個思路給我們帶來了不可避免的兩個問題:

  • load 是一個“結果”,如果根據 load 的情況來調節流量的通過率,那么就始終有延遲性。也就意味着通過率的任何調整,都會過一段時間才能看到效果。當前通過率是使 load 惡化的一個動作,那么也至少要過 1 秒之后才能觀測到;同理,如果當前通過率調整是讓 load 好轉的一個動作,也需要 1 秒之后才能繼續調整,這樣就浪費了系統的處理能力。所以我們看到的曲線,總是會有抖動。
  • 恢復慢。想象一下這樣的一個場景(真實),出現了這樣一個問題,下游應用不可靠,導致應用 RT 很高,從而 load 到了一個很高的點。過了一段時間之后下游應用恢復了,應用 RT 也相應減少。這個時候,其實應該大幅度增大流量的通過率;但是由於這個時候 load 仍然很高,通過率的恢復仍然不高。

系統保護的目標是 在系統不被拖垮的情況下,提高系統的吞吐率,而不是 load 一定要到低於某個閾值。如果我們還是按照固有的思維,超過特定的 load 就禁止流量進入,系統 load 恢復就放開流量,這樣做的結果是無論我們怎么調參數,調比例,都是按照果來調節因,都無法取得良好的效果。

Sentinel 在系統自適應保護的做法是,用 load1 作為啟動自適應保護的因子,而允許通過的流量由處理請求的能力,即請求的響應時間以及當前系統正在處理的請求速率來決定。

系統保護規則的應用

系統規則支持以下的模式:

  • Load 自適應(僅對 Linux/Unix-like 機器生效):系統的 load1 作為啟發指標,進行自適應系統保護。當系統 load1 超過設定的啟發值,且系統當前的並發線程數超過估算的系統容量時才會觸發系統保護(BBR 階段)。系統容量由系統的 maxQps * minRt 估算得出。設定參考值一般是 CPU cores * 2.5

  • CPU usage(1.5.0+ 版本):當系統 CPU 使用率超過閾值即觸發系統保護(取值范圍 0.0-1.0),比較靈敏。

  • 平均 RT:當單台機器上所有入口流量的平均 RT 達到閾值即觸發系統保護,單位是毫秒。

  • 並發線程數:當單台機器上所有入口流量的並發線程數達到閾值即觸發系統保護。

  • 入口 QPS:當單台機器上所有入口流量的 QPS 達到閾值即觸發系統保護。

系統保護規則是從應用級別的入口流量進行控制,從單台機器的 load、CPU 使用率、平均 RT、入口 QPS 和並發線程數等幾個維度監控應用指標,讓系統盡可能跑在最大吞吐量的同時保證系統整體的穩定性。

系統保護規則是應用整體維度的,而不是資源維度的,並且僅對入口流量生效。入口流量指的是進入應用的流量(EntryType.IN),比如 Web 服務或 Dubbo 服務端接收的請求,都屬於入口流量。

系統規則的參數說明:

  • highestSystemLoad 最大的 load1,參考值 -1 (不生效)
  • avgRt 所有入口流量的平均響應時間 -1 (不生效)
  • maxThread 入口流量的最大並發數 -1 (不生效)
  • qps 所有入口資源的 QPS -1 (不生效)

硬編碼的方式定義流量控制規則如下:

       List<SystemRule> srules = new ArrayList<>();
        SystemRule srule = new SystemRule();
        srule.setAvgRt(3000);
        srules.add(srule);
        SystemRuleManager.loadRules(srules);

網頁限流規則配置

在這里插入圖片描述

6 黑白名單規則

很多時候,我們需要根據調用方來限制資源是否通過,這時候可以使用 Sentinel 的訪問控制(黑白名單)的功能。黑白名單根據資源的請求來源(origin)限制資源是否通過,若配置白名單則只有請求來源位於白名單內時才可通過;若配置黑名單則請求來源位於黑名單時不通過,其余的請求通過。

調用方信息通過 ContextUtil.enter(resourceName, origin) 方法中的 origin 參數傳入。

訪問控制規則 (AuthorityRule)

授權規則,即黑白名單規則(AuthorityRule)非常簡單,主要有以下配置項:

  • resource:資源名,即限流規則的作用對象
  • limitApp:對應的黑名單/白名單,不同 origin 用 , 分隔,如 appA,appB
  • strategy:限制模式,AUTHORITY_WHITE 為白名單模式,AUTHORITY_BLACK 為黑名單模式,默認為白名單模式 比如我們希望控制對資源 test 的訪問設置白名單,只有來源為 appA 和 appB 的請求才可通過,則可以配置如下白名單規則:
AuthorityRule rule = new AuthorityRule();
rule.setResource("test");
rule.setStrategy(RuleConstant.AUTHORITY_WHITE);
rule.setLimitApp("appA,appB");
AuthorityRuleManager.loadRules(Collections.singletonList(rule));

7 核心組件

Resource

resource是sentinel中最重要的一個概念,sentinel通過資源來保護具體的業務代碼或其他后方服務。sentinel把復雜的邏輯給屏蔽掉了,用戶只需要為受保護的代碼或服務定義一個資源,然后定義規則就可以了,剩下的通通交給sentinel來處理了。並且資源和規則是解耦的,規則甚至可以在運行時動態修改。定義完資源后,就可以通過在程序中埋點來保護你自己的服務了,埋點的方式有兩種:

  • try-catch 方式(通過 SphU.entry(...)),當 catch 到BlockException時執行異常處理(或fallback)
  • if-else 方式(通過 SphO.entry(...)),當返回 false 時執行異常處理(或fallback)

以上這兩種方式都是通過硬編碼的形式定義資源然后進行資源埋點的,對業務代碼的侵入太大,從0.1.1版本開始,sentinel加入了注解的支持,可以通過注解來定義資源,具體的注解為:SentinelResource 。通過注解除了可以定義資源外,還可以指定 blockHandler 和 fallback 方法。

在sentinel中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper。顧名思義,StringResourceWrapper 是通過對一串字符串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。

Context

Context是對資源操作時的上下文環境,每個資源操作(針對Resource進行的entry/exit)必須屬於一個Context,如果程序中未指定Context,會創建name為"sentinel_default_context"的默認Context。一個Context生命周期內可能有多個資源操作,Context生命周期內的最后一個資源exit時會清理該Context,這也預示這整個Context生命周期的結束。Context主要屬性如下:

public class Context {
   // context名字,默認名字 "sentinel_default_context"
   private final String name;
   // context入口節點,每個context必須有一個entranceNode
   private DefaultNode entranceNode;
   // context當前entry,Context生命周期中可能有多個Entry,所有curEntry會有變化
   private Entry curEntry;
   // The origin of this context (usually indicate different invokers, e.g. service consumer name or origin IP).
   private String origin = "";
   private final boolean async;
}

注意:一個Context生命期內Context只能初始化一次,因為是存到ThreadLocal中,並且只有在非null時才會進行初始化。

如果想在調用 SphU.entry() 或 SphO.entry() 前,自定義一個context,則通過ContextUtil.enter()方法來創建。context是保存在ThreadLocal中的,每次執行的時候會優先到ThreadLocal中獲取,為null時會調用 MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType())創建一個context。當Entry執行exit方法時,如果entry的parent節點為null,表示是當前Context中最外層的Entry了,此時將ThreadLocal中的context清空。

Context的創建與銷毀

首先我們要清楚的一點就是,每次執行entry()方法,試圖沖破一個資源時,都會生成一個上下文。這個上下文中會保存着調用鏈的根節點和當前的入口。

Context是通過ContextUtil創建的,具體的方法是trueEntry,代碼如下:

protected static Context trueEnter(String name, String origin) {
    // 先從ThreadLocal中獲取
    Context context = contextHolder.get();
    if (context == null) {
        // 如果ThreadLocal中獲取不到Context
        // 則根據name從map中獲取根節點,只要是相同的資源名,就能直接從map中獲取到node
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 省略部分代碼
            try {
                LOCK.lock();
                node = contextNameNodeMap.get(name);
                if (node == null) {
                    // 省略部分代碼
                    // 創建一個新的入口節點
                    node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                    Constants.ROOT.addChild(node);
                    // 省略部分代碼
                }
            } finally {
                LOCK.unlock();
            }
        }
        // 創建一個新的Context,並設置Context的根節點,即設置EntranceNode
        context = new Context(node, name);
        context.setOrigin(origin);
        // 將該Context保存到ThreadLocal中去
        contextHolder.set(context);
    }
    return context;
}

上面的代碼中我省略了部分代碼,只保留了核心的部分。從源碼中還是可以比較清晰的看出生成Context的過程:

  • 1.先從ThreadLocal中獲取,如果能獲取到直接返回,如果獲取不到則繼續第2步
  • 2.從一個static的map中根據上下文的名稱獲取,如果能獲取到則直接返回,否則繼續第3步
  • 3.加鎖后進行一次double check,如果還是沒能從map中獲取到,則創建一個EntranceNode,並把該EntranceNode添加到一個全局的ROOT節點中去,然后將該節點添加到map中去(這部分代碼在上述代碼中省略了)
  • 4.根據EntranceNode創建一個上下文,並將該上下文保存到ThreadLocal中去,下一個請求可以直接獲取

那保存在ThreadLocal中的上下文什么時候會清除呢?從代碼中可以看到具體的清除工作在ContextUtil的exit方法中,當執行該方法時,會將保存在ThreadLocal中的context對象清除,具體的代碼非常簡單,這里就不貼代碼了。

那ContextUtil.exit方法什么時候會被調用呢?有兩種情況:一是主動調用ContextUtil.exit的時候,二是當一個入口Entry要退出,執行該Entry的trueExit方法的時候,此時會觸發ContextUtil.exit的方法。但是有一個前提,就是當前Entry的父Entry為null時,此時說明該Entry已經是最頂層的根節點了,可以清除context。

Entry

剛才在Context身影中也看到了Entry的出現,現在就談談Entry。每次執行 SphU.entry() 或 SphO.entry() 都會返回一個Entry,Entry表示一次資源操作,內部會保存當前invocation信息。在一個Context生命周期中多次資源操作,也就是對應多個Entry,這些Entry形成parent/child結構保存在Entry實例中,entry類CtEntry結構如下:

class CtEntry extends Entry {
   protected Entry parent = null;
   protected Entry child = null;

   protected ProcessorSlot<Object> chain;
   protected Context context;
}
public abstract class Entry implements AutoCloseable {
   private long createTime;
   private Node curNode;
   /**
    * {@link Node} of the specific origin, Usually the origin is the Service Consumer.
    */
   private Node originNode;
   private Throwable error; // 是否出現異常
   protected ResourceWrapper resourceWrapper; // 資源信息
}

Entry實例代碼中出現了Node,這個又是什么東東呢 😦,咱們接着往下看:

DefaultNode

Node(關於StatisticNode的討論放到下一小節)默認實現類DefaultNode,該類還有一個子類EntranceNode;context有一個entranceNode屬性,Entry中有一個curNode屬性。

  • EntranceNode:該類的創建是在初始化Context時完成的(ContextUtil.trueEnter方法),注意該類是針對Context維度的,也就是一個context有且僅有一個EntranceNode。
  • DefaultNode:該類的創建是在NodeSelectorSlot.entry完成的,當不存在context.name對應的DefaultNode時會新建(new DefaultNode(resourceWrapper, null),對應resouce)並保存到本地緩存(NodeSelectorSlot中private volatile Map<String, DefaultNode> map);獲取到context.name對應的DefaultNode后會將該DefaultNode設置到當前context的curEntry.curNode屬性,也就是說,在NodeSelectorSlot中是一個context有且僅有一個DefaultNode。

看到這里,你是不是有疑問?為什么一個context有且僅有一個DefaultNode,我們的resouece跑哪去了呢,其實,這里的一個context有且僅有一個DefaultNode是在NodeSelectorSlot范圍內,NodeSelectorSlot是ProcessorSlotChain中的一環,獲取ProcessorSlotChain是根據Resource維度來的。總結為一句話就是:針對同一個Resource,多個context對應多個DefaultNode;針對不同Resource,(不管是否是同一個context)對應多個不同DefaultNode。這還沒看明白 : (,好吧,我不bb了,上圖吧:

public class DefaultNode extends StatisticNode {
private ResourceWrapper id;
/**
* The list of all child nodes.
* 子節點集合
/
private volatile Set childList = new HashSet<>();
/
*
* Associated cluster node.
*/
private ClusterNode clusterNode;
}

一個Resouce只有一個clusterNode,多個defaultNode對應一個clusterNode,如果defaultNode.clusterNode為null,則在ClusterBuilderSlot.entry中會進行初始化。

同一個Resource,對應同一個ProcessorSlotChain,這塊處理邏輯在lookProcessChain方法中,如下:

ProcessorSlot lookProcessChain(ResourceWrapper resourceWrapper) {
ProcessorSlotChain chain = chainMap.get(resourceWrapper);
if (chain == null) {
synchronized (LOCK) {
chain = chainMap.get(resourceWrapper);
if (chain == null) {
// Entry size limit.
if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
return null;
}

           chain = SlotChainProvider.newSlotChain();
           Map<ResourceWrapper, ProcessorSlotChain> newMap = newHashMap<ResourceWrapper, ProcessorSlotChain>(
               chainMap.size() + 1);
           newMap.putAll(chainMap);
           newMap.put(resourceWrapper, chain);
           chainMap = newMap;
      }
  }

}
return chain;
}

StatisticNode

StatisticNode中保存了資源的實時統計數據(基於滑動時間窗口機制),通過這些統計數據,sentinel才能進行限流、降級等一系列操作。StatisticNode屬性如下:

public class StatisticNode implements Node {
   /**
    * 秒級的滑動時間窗口(時間窗口單位500ms)
    */
   private transient volatile Metric rollingCounterInSecond = newArrayMetric(SampleCountProperty.SAMPLE_COUNT,
       IntervalProperty.INTERVAL);
   /**
    * 分鍾級的滑動時間窗口(時間窗口單位1s)
    */
   private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000,false);
   /**
    * The counter for thread count.

 線程個數用戶觸發線程數流控
*/
private LongAdder curThreadNum = new LongAdder();
}
public class ArrayMetric implements Metric {
      private final LeapArray<MetricBucket> data;
}
public class MetricBucket {
// 保存統計值
private final LongAdder[] counters;
// 最小rt
private volatile long minRt;
}

其中MetricBucket.counters數組大小為MetricEvent枚舉值的個數,每個枚舉對應一個統計項,比如PASS表示通過個數,限流可根據通過的個數和設置的限流規則配置count大小比較,得出是否觸發限流操作,所有枚舉值如下:

public enum MetricEvent {
   PASS, // Normal pass.
   BLOCK, // Normal block.
   EXCEPTION,
   SUCCESS,
   RT,
   OCCUPIED_PASS
}

8 插槽Slot

slot是另一個sentinel中非常重要的概念,sentinel的工作流程就是圍繞着一個個插槽所組成的插槽鏈來展開的。需要注意的是每個插槽都有自己的職責,他們各司其職完好的配合,通過一定的編排順序,來達到最終的限流降級的目的。默認的各個插槽之間的順序是固定的,因為有的插槽需要依賴其他的插槽計算出來的結果才能進行工作。

但是這並不意味着我們只能按照框架的定義來,sentinel 通過 SlotChainBuilder 作為 SPI 接口,使得 Slot Chain 具備了擴展的能力。我們可以通過實現 SlotsChainBuilder 接口加入自定義的 slot 並自定義編排各個 slot 之間的順序,從而可以給 sentinel 添加自定義的功能。

那SlotChain是在哪創建的呢?是在 CtSph.lookProcessChain() 方法中創建的,並且該方法會根據當前請求的資源先去一個靜態的HashMap中獲取,如果獲取不到才會創建,創建后會保存到HashMap中。這就意味着,同一個資源會全局共享一個SlotChain。默認生成ProcessorSlotChain為:

// DefaultSlotChainBuilder
public ProcessorSlotChain build() {
   ProcessorSlotChain chain = new DefaultProcessorSlotChain();
   chain.addLast(new NodeSelectorSlot());
   chain.addLast(new ClusterBuilderSlot());
   chain.addLast(new LogSlot());
   chain.addLast(new StatisticSlot());
   chain.addLast(new SystemSlot());
   chain.addLast(new AuthoritySlot());
   chain.addLast(new FlowSlot());
   chain.addLast(new DegradeSlot());

   return chain;

這里大概的介紹下每種Slot的功能職責:

  • NodeSelectorSlot 負責收集資源的路徑,並將這些資源的調用路徑,以樹狀結構存儲起來,用於根據調用路徑來限流降級;
  • ClusterBuilderSlot 則用於存儲資源的統計信息以及調用者信息,例如該資源的 RT, QPS, thread count 等等,這些信息將用作為多維度限流,降級的依據;
  • StatisticsSlot 則用於記錄,統計不同維度的 runtime 信息;
  • SystemSlot 則通過系統的狀態,例如 load1 等,來控制總的入口流量;
  • AuthoritySlot 則根據黑白名單,來做黑白名單控制;
  • FlowSlot 則用於根據預設的限流規則,以及前面 slot 統計的狀態,來進行限流;
  • DegradeSlot 則通過統計信息,以及預設的規則,來做熔斷降級;

每個Slot執行完業務邏輯處理后,會調用fireEntry()方法,該方法將會觸發下一個節點的entry方法,下一個節點又會調用他的fireEntry,以此類推直到最后一個Slot,由此就形成了sentinel的責任鏈。

下面我們就來詳細研究下這些Slot的原理。

NodeSelectorSlot

NodeSelectorSlot 是用來構造調用鏈的,具體的是將資源的調用路徑,封裝成一個一個的節點,再組成一個樹狀的結構來形成一個完整的調用鏈,NodeSelectorSlot是所有Slot中最關鍵也是最復雜的一個Slot,這里涉及到以下幾個核心的概念:

  • Resource

資源是 Sentinel 的關鍵概念。它可以是 Java 應用程序中的任何內容,例如,由應用程序提供的服務,或由應用程序調用的其它服務,甚至可以是一段代碼。

只要通過 Sentinel API 定義的代碼,就是資源,能夠被 Sentinel 保護起來。大部分情況下,可以使用方法簽名,URL,甚至服務名稱作為資源名來標示資源。

簡單來說,資源就是 Sentinel 用來保護系統的一個媒介。源碼中用來包裝資源的類是:com.alibaba.csp.sentinel.slotchain.ResourceWrapper,他有兩個子類:StringResourceWrapperMethodResourceWrapper,通過名字就知道可以將一段字符串或一個方法包裝為一個資源。

打個比方,我有一個服務A,請求非常多,經常會被陡增的流量沖垮,為了防止這種情況,簡單的做法,我們可以定義一個 Sentinel 的資源,通過該資源來對請求進行調整,使得允許通過的請求不會把服務A搞崩潰。

img

每個資源的狀態也是不同的,這取決於資源后端的服務,有的資源可能比較穩定,有的資源可能不太穩定。那么在整個調用鏈中,Sentinel 需要對不穩定資源進行控制。當調用鏈路中某個資源出現不穩定,例如,表現為 timeout,或者異常比例升高的時候,則對這個資源的調用進行限制,並讓請求快速失敗,避免影響到其它的資源,最終導致雪崩的后果。

  • Context

上下文是一個用來保存調用鏈當前狀態的元數據的類,每次進入一個資源時,就會創建一個上下文。相同的資源名可能會創建多個上下文。一個Context中包含了三個核心的對象:

1)當前調用鏈的根節點:EntranceNode

2)當前的入口:Entry

3)當前入口所關聯的節點:Node

上下文中只會保存一個當前正在處理的入口Entry,另外還會保存調用鏈的根節點。需要注意的是,每次進入一個新的資源時,都會創建一個新的上下文。

  • Entry

每次調用 SphU#entry() 都會生成一個Entry入口,該入口中會保存了以下數據:入口的創建時間,當前入口所關聯的節點,當前入口所關聯的調用源對應的節點。Entry是一個抽象類,他只有一個實現類,在CtSph中的一個靜態類:CtEntry

  • Node

節點是用來保存某個資源的各種實時統計信息的,他是一個接口,通過訪問節點,就可以獲取到對應資源的實時狀態,以此為依據進行限流和降級操作。

可能看到這里,大家還是比較懵,這么多類到底有什么用,接下來就讓我們更進一步,挖掘一下這些類的作用,在這之前,我先給大家展示一下他們之間的關系,如下圖所示:

img

這里把幾種Node的作用先大概介紹下:

節點 作用
StatisticNode 執行具體的資源統計操作
DefaultNode 該節點持有指定上下文中指定資源的統計信息,當在同一個上下文中多次調用entry方法時,該節點可能下會創建有一系列的子節點。 另外每個DefaultNode中會關聯一個ClusterNode
ClusterNode 該節點中保存了資源的總體的運行時統計信息,包括rt,線程數,qps等等,相同的資源會全局共享同一個ClusterNode,不管他屬於哪個上下文
EntranceNode 該節點表示一棵調用鏈樹的入口節點,通過他可以獲取調用鏈樹中所有的子節點

調用鏈樹

當在一個上下文中多次調用了 SphU#entry() 方法時,就會創建一棵調用鏈樹。具體的代碼在entry方法中創建CtEntry對象時:

CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
    super(resourceWrapper);
    this.chain = chain;
    this.context = context;
    // 獲取「上下文」中上一次的入口
    parent = context.getCurEntry();
    if (parent != null) {
        // 然后將當前入口設置為上一次入口的子節點
        ((CtEntry)parent).child = this;
    }
    // 設置「上下文」的當前入口為該類本身
    context.setCurEntry(this);
}

這里可能看代碼沒有那么直觀,可以用一些圖形來描述一下這個過程。

構造樹干

創建context

context的創建在上面已經分析過了,初始化的時候,context中的curEntry屬性是沒有值的,如下圖所示:

img

創建Entry

每創建一個新的Entry對象時,都會重新設置context的curEntry,並將context原來的curEntry設置為該新Entry對象的父節點,如下圖所示:

img

退出Entry

某個Entry退出時,將會重新設置context的curEntry,當該Entry是最頂層的一個入口時,將會把ThreadLocal中保存的context也清除掉,如下圖所示:

img

構造葉子節點

上面的過程是構造了一棵調用鏈的樹,但是這棵樹只有樹干,沒有葉子,那葉子節點是在什么時候創建的呢?DefaultNode就是葉子節點,在葉子節點中保存着目標資源在當前狀態下的統計信息。通過分析,我們知道了葉子節點是在NodeSelectorSlot的entry方法中創建的。具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable {
    // 根據「上下文」的名稱獲取DefaultNode
    // 多線程環境下,每個線程都會創建一個context,
    // 只要資源名相同,則context的名稱也相同,那么獲取到的節點就相同
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 如果當前「上下文」中沒有該節點,則創建一個DefaultNode節點
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
                // 省略部分代碼
            }
            // 將當前node作為「上下文」的最后一個節點的子節點添加進去
            // 如果context的curEntry.parent.curNode為null,則添加到entranceNode中去
            // 否則添加到context的curEntry.parent.curNode中去
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }
    // 將該節點設置為「上下文」中的當前節點
    // 實際是將當前節點賦值給context中curEntry的curNode
    // 在Context的getLastNode中會用到在此處設置的curNode
    context.setCurNode(node);
    fireEntry(context, resourceWrapper, node, count, args);
}

上面的代碼可以分解成下面這些步驟:
1)獲取當前上下文對應的DefaultNode,如果沒有的話會為當前的調用新生成一個DefaultNode節點,它的作用是對資源進行各種統計度量以便進行流控;
2)將新創建的DefaultNode節點,添加到context中,作為「entranceNode」或者「curEntry.parent.curNode」的子節點;
3)將DefaultNode節點,添加到context中,作為「curEntry」的curNode。

上面的第2步,不是每次都會執行。我們先看第3步,把當前DefaultNode設置為context的curNode,實際上是把當前節點賦值給context中curEntry的curNode,用圖形表示就是這樣:

img

多次創建不同的Entry,並且執行NodeSelectorSlot的entry方法后,就會變成這樣一棵調用鏈樹:

img

PS:這里圖中的node0,node1,node2可能是相同的node,因為在同一個context中從map中獲取的node是同一個,這里只是為了表述的更清楚所以用了不同的節點名。

保存子節點

上面已經分析了葉子節點的構造過程,葉子節點是保存在各個Entry的curNode屬性中的。

我們知道context中只保存了入口節點和當前Entry,那子節點是什么時候保存的呢,其實子節點就是上面代碼中的第2步中保存的。

下面我們來分析上面的第2步的情況:

第一次調用NodeSelectorSlot的entry方法時,map中肯定是沒有DefaultNode的,那就會進入第2步中,創建一個node,創建完成后會把該節點加入到context的lastNode的子節點中去。我們先看一下context的getLastNode方法:

public Node getLastNode() {
    // 如果curEntry不存在時,返回entranceNode
    // 否則返回curEntry的lastNode,
    // 需要注意的是curEntry的lastNode是獲取的parent的curNode,
    // 如果每次進入的資源不同,就會每次都創建一個CtEntry,則parent為null,
    // 所以curEntry.getLastNode()也為null
    if (curEntry != null && curEntry.getLastNode() != null) {
        return curEntry.getLastNode();
    } else {
        return entranceNode;
    }
}

代碼中我們可以知道,lastNode的值可能是context中的entranceNode也可能是curEntry.parent.curNode,但是他們都是「DefaultNode」類型的節點,DefaultNode的所有子節點是保存在一個HashSet中的。

第一次調用getLastNode方法時,context中curEntry是null,因為curEntry是在第3步中才賦值的。所以,lastNode最初的值就是context的entranceNode。那么將node添加到entranceNode的子節點中去之后就變成了下面這樣:

img

緊接着再進入一次,資源名不同,會再次生成一個新的Entry,上面的圖形就變成下圖這樣:

img

此時再次調用context的getLastNode方法,因為此時curEntry的parent不再是null了,所以獲取到的lastNode是curEntry.parent.curNode,在上圖中可以很方便的看出,這個節點就是node0。那么把當前節點node1添加到lastNode的子節點中去,上面的圖形就變成下圖這樣:

img

然后將當前node設置給context的curNode,上面的圖形就變成下圖這樣:

img

假如再創建一個Entry,然后再進入一次不同的資源名,上面的圖就變成下面這樣:

img

至此NodeSelectorSlot的基本功能已經大致分析清楚了。

PS:以上的分析是基於每次執行SphU.entry(name)時,資源名都是不一樣的前提下。如果資源名都一樣的話,那么生成的node都相同,則只會再第一次把node加入到entranceNode的子節點中去,其他的時候,只會創建一個新的Entry,然后替換context中的curEntry的值。

ClusterBuilderSlot

NodeSelectorSlot的entry方法執行完之后,會調用fireEntry方法,此時會觸發ClusterBuilderSlot的entry方法。

ClusterBuilderSlot的entry方法比較簡單,具體代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                // Create the cluster node.
                clusterNode = Env.nodeBuilder.buildClusterNode();
                // 將clusterNode保存到全局的map中去
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);
 
                clusterNodeMap = newMap;
            }
        }
    }
    // 將clusterNode塞到DefaultNode中去
    node.setClusterNode(clusterNode);
 
    // 省略部分代碼
 
    fireEntry(context, resourceWrapper, node, count, args);
}

NodeSelectorSlot的職責比較簡單,主要做了兩件事:

一、為每個資源創建一個clusterNode,然后把clusterNode塞到DefaultNode中去

二、將clusterNode保持到全局的map中去,用資源作為map的key

PS:一個資源只有一個ClusterNode,但是可以有多個DefaultNode

StatistcSlot

StatisticSlot負責來統計資源的實時狀態,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    try {
        // 觸發下一個Slot的entry方法
        fireEntry(context, resourceWrapper, node, count, args);
        // 如果能通過SlotChain中后面的Slot的entry方法,說明沒有被限流或降級
        // 統計信息
        node.increaseThreadNum();
        node.addPassRequest();
        // 省略部分代碼
    } catch (BlockException e) {
        context.getCurEntry().setError(e);
        // Add block count.
        node.increaseBlockedQps();
        // 省略部分代碼
        throw e;
    } catch (Throwable e) {
        context.getCurEntry().setError(e);
        // Should not happen
        node.increaseExceptionQps();
        // 省略部分代碼
        throw e;
    }
}

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    DefaultNode node = (DefaultNode)context.getCurNode();
    if (context.getCurEntry().getError() == null) {
        long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
        if (rt > Constants.TIME_DROP_VALVE) {
            rt = Constants.TIME_DROP_VALVE;
        }
        node.rt(rt);
        // 省略部分代碼
        node.decreaseThreadNum();
        // 省略部分代碼
    } 
    fireExit(context, resourceWrapper, count);
}

代碼分成了兩部分,第一部分是entry方法,該方法首先會觸發后續slot的entry方法,即SystemSlot、FlowSlot、DegradeSlot等的規則,如果規則不通過,就會拋出BlockException,則會在node中統計被block的數量。反之會在node中統計通過的請求數和線程數等信息。第二部分是在exit方法中,當退出該Entry入口時,會統計rt的時間,並減少線程數。

這些統計的實時數據會被后續的校驗規則所使用,具體的統計方式是通過 滑動窗口 來實現的。

SystemSlot

SystemSlot就是根據總的請求統計信息,來做流控,主要是防止系統被搞垮,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    SystemRuleManager.checkSystem(resourceWrapper);
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
    if (resourceWrapper == null) {
        return;
    }
    // Ensure the checking switch is on.
    if (!checkSystemStatus.get()) {
        return;
    }

    // for inbound traffic only
    if (resourceWrapper.getEntryType() != EntryType.IN) {
        return;
    }

    // total qps
    double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
    if (currentQps > qps) {
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    }

    // total thread
    int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) {
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    }

    double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) {
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    }

    // BBR算法
    // load. BBR algorithm.
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
        if (!checkBbr(currentThread)) {
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        }
    }

    // cpu usage
    if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) {
        throw new SystemBlockException(resourceWrapper.getName(), "cpu");
    }
}

其中的Constants.ENTRY_NODE是一個全局的ClusterNode,該節點的值是在StatisticsSlot中進行統計的。

當前的統計值和系統配置的進行比較,各個維度超過范圍拋BlockException

AuthoritySlot

AuthoritySlot做的事也比較簡單,主要是根據黑白名單進行過濾,只要有一條規則校驗不通過,就拋出異常。

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args)
    throws Throwable {
    checkBlackWhiteAuthority(resourceWrapper, context);
    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkBlackWhiteAuthority(ResourceWrapper resource, Context context) throws AuthorityException {
    // 通過監聽來的規則集
    Map<String, Set<AuthorityRule>> authorityRules = AuthorityRuleManager.getAuthorityRules();

    if (authorityRules == null) {
        return;
    }


    // 根據資源名稱獲取相應的規則
    Set<AuthorityRule> rules = authorityRules.get(resource.getName());
    if (rules == null) {
        return;
    }

    for (AuthorityRule rule : rules) {
        // 黑名單白名單驗證
        // 只要有一條規則校驗不通過,就拋出AuthorityException
        if (!AuthorityRuleChecker.passCheck(rule, context)) {
            throw new AuthorityException(context.getOrigin(), rule);
        }
    }
}

FlowSlot

FlowSlot主要是根據前面統計好的信息,與設置的限流規則進行匹配校驗,如果規則校驗不通過則進行限流,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    checkFlow(resourceWrapper, context, node, count, prioritized);

    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
    throws BlockException {
    checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}

DegradeSlot

DegradeSlot主要是根據前面統計好的信息,與設置的降級規則進行匹配校驗,如果規則校驗不通過則進行降級,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable {
    performChecking(context, resourceWrapper);

    fireEntry(context, resourceWrapper, node, count, prioritized, args);
}

void performChecking(Context context, ResourceWrapper r) throws BlockException {
    List<CircuitBreaker> circuitBreakers = 		
        DegradeRuleManager.getCircuitBreakers(r.getName());
    
    if (circuitBreakers == null || circuitBreakers.isEmpty()) {
        return;
    }
    for (CircuitBreaker cb : circuitBreakers) {
        if (!cb.tryPass(context)) {
            throw new DegradeException(cb.getRule().getLimitApp(), cb.getRule());
        }
    }
}

DefaultProcessorSlotChain

Chain 是鏈條的意思,從build的方法可看出,ProcessorSlotChain 是一個鏈表,里面添加了很多個 Slot。都是 ProcessorSlot 的子類。具體的實現需要到 DefaultProcessorSlotChain 中去看。

public class DefaultProcessorSlotChain extends ProcessorSlotChain {

    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {

        @Override
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
            throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
        }

        @Override
        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }

    };
    AbstractLinkedProcessorSlot<?> end = first;

    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }

    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }

    /**
     * Same as {@link #addLast(AbstractLinkedProcessorSlot)}.
     *
     * @param next processor to be added.
     */
    @Override
    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        addLast(next);
    }

    @Override
    public AbstractLinkedProcessorSlot<?> getNext() {
        return first.getNext();
    }

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
        throws Throwable {
        first.transformEntry(context, resourceWrapper, t, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        first.exit(context, resourceWrapper, count, args);
    }

}

DefaultProcessorSlotChain中有兩個AbstractLinkedProcessorSlot類型的變量:first和end,這就是鏈表的頭結點和尾節點。

創建DefaultProcessorSlotChain對象時,首先創建了首節點,然后把首節點賦值給了尾節點,可以用下圖表示:
在這里插入圖片描述
將第一個節點添加到鏈表中后,整個鏈表的結構變成了如下圖這樣:

在這里插入圖片描述
將所有的節點都加入到鏈表中后,整個鏈表的結構變成了如下圖所示:

在這里插入圖片描述
這樣就將所有的Slot對象添加到了鏈表中去了,每一個Slot都是繼承自AbstractLinkedProcessorSlot。而AbstractLinkedProcessorSlot是一種責任鏈的設計,每個對象中都有一個next屬性,指向的是另一個AbstractLinkedProcessorSlot對象。其實責任鏈模式在很多框架中都有,比如Netty中是通過pipeline來實現的。

知道了SlotChain是如何創建的了,那接下來就要看下是如何執行Slot的entry方法的了

從這里可以看到,從fireEntry方法中就開始傳遞執行entry了,這里會執行當前節點的下一個節點transformEntry方法,上面已經分析過了,transformEntry方法會觸發當前節點的entry,也就是說fireEntry方法實際是觸發了下一個節點的entry方法。
在這里插入圖片描述

從最初的調用Chain的entry()方法,轉變成了調用SlotChain中Slot的entry()方法。從 @SpiOrder(-10000) 知道,SlotChain中的第一個Slot節點是NodeSelectorSlot。

slot總結

sentinel的限流降級等功能,主要是通過一個SlotChain實現的。在鏈式插槽中,有7個核心的Slot,這些Slot各司其職,可以分為以下幾種類型:

一、進行資源調用路徑構造的NodeSelectorSlot和ClusterBuilderSlot

二、進行資源的實時狀態統計的StatisticsSlot

三、進行系統保護,限流,降級等規則校驗的SystemSlot、AuthoritySlot、FlowSlot、DegradeSlot

后面幾個Slot依賴於前面幾個Slot統計的結果。至此,每種Slot的功能已經基本分析清楚了。

9 .sentinel滑動窗口實現原理

基本原理

滑動窗口可以先拆為滑動跟窗口兩個詞,先介紹下窗口,你可以這么理解,一段是時間就是窗口,比如說我們可以把這個1s認為是1個窗口
這個樣子我們就能將1分鍾就可以划分成60個窗口了,這個沒毛病吧。如下圖我們就分成了60個窗口(這個多了我們就畫5個表示一下)
在這里插入圖片描述
比如現在處於第1秒上,那1s那個窗口就是當前窗口,就如下圖中紅框表示。
在這里插入圖片描述
好了,窗口就介紹完了,現在在來看下滑動,滑動很簡單,比如說現在時間由第1秒變成了第2秒,就是從當前這個窗口---->下一個窗口就可以了,這個時候下一個窗口就變成了當前窗口,之前那個當前窗口就變成了上一個窗口,這個過程其實就是滑動。
在這里插入圖片描述

好了,介紹完了滑動窗口,我們再來介紹下這個sentinel的滑動窗口的實現原理。
其實你要是理解了上面這個滑動窗口的意思,sentinel實現原理就簡單了。
先是介紹下窗口中里面都存儲些啥。也就是上面這個小框框都有啥。

  1. 它得有個開始時間吧,不然你怎么知道這個窗口是什么時候開始的
  2. 還得有個窗口的長度吧,不然你咋知道窗口啥時候結束,通過這個開始時間+窗口長度=窗口結束時間,就比如說上面的1s,間隔1s
  3. 最后就是要在這個窗口里面統計的東西,你總不能白搞些窗口,搞些滑動吧。所以這里就存儲了一堆要統計的指標(qps,rt等等)

說完了這一個小窗口里面的東西,就得來說說是怎么划分這個小窗口,怎么管理這些小窗口的了,也就是我們的視野得往上提高一下了,不能總聚在這個小窗口上。

  1. 要知道有多少個小窗口,在sentinel中也就是sampleCount,比如說我們有60個窗口。
  2. 還有就是intervalInMs,這個intervalInMs是用來計算這個窗口長度的,intervalInMs/窗口數量= 窗口長度。也就是我給你1分鍾,你給我分成60個窗口,這個時候窗口長度就是1s了,那如果我給你1s,你給我分2個窗口,這個時候窗口長度就是500毫秒了,這個1分鍾,就是intervalInMs。
  3. 再就是存儲這個窗口的容器(這里是數組),畢竟那么多窗口,還得提供計算當前時間窗口的方法等等

最后我們就來看看這個當前時間窗口是怎么計算的。
咱們就拿 60個窗口,這個60個窗口放在數組中,窗口長度是1s 來計算,看看當前時間戳的一個時間窗口是是在數組中哪個位置。

比如說當前時間戳是1609085401454 ms,算出秒 = 1609085401454 /1000(窗口長度)

在數組的位置 = 算出秒 %數組長度

我們再來計算下 某個時間戳對應窗口的起始時間,還是以1609085401454 來計算

窗口startTime = 1609085401454 - 1609085401454%1000(窗口長度)=454

這里1609085401454%1000(窗口長度) 能算出來它的毫秒值,也就是454 , 減去這個后就變成了1609085401000

好了,sentinel 滑動窗口原理就介紹完成了。

2.sentinel使用滑動窗口都統計啥

我們來介紹下使用這個滑動窗口都來統計啥

public enum MetricEvent {
    /**
     * Normal pass.
     */
    PASS,// 通過
    /**
     * Normal block.
     */
    BLOCK,// 拒絕的
    EXCEPTION,// 異常
    SUCCESS,//成功
    RT,// 耗時
    /**
     * Passed in future quota (pre-occupied, since 1.5.0).
     */
    OCCUPIED_PASS
}
1234567891011121314151617

這是最基本的指標,然后通過這些指標,又可以計算出來比如說最大,最小,平均等等的一些指標。

3.滑動窗口源碼實現

我們先來看下這個窗口里面的統計指標的實現 MetricBucket

3.1 MetricBucket

這個MetricBucket 是由LongAdder數組組成的,一個LongAdder就是一個MetricEvent ,也就是第二小節里面的PASS ,BLOCK等等。
我們稍微看下就可以了
在這里插入圖片描述
可以看到它在實例化的時候創建一個LongAdder 數據,個數就是那堆event的數量。這個LongAdder 是jdk8里面的原子操作類,你可以把它簡單認為AtomicLong。然后下面就是一堆get 跟add 的方法了,這里我們就不看了。
接下來再來看看那這個窗口的實現WindowWrap類

3.2 WindowWrap

先來看下這個成員
在這里插入圖片描述
窗口長度,窗口startTime ,指標統計的 都有了,下面的就沒啥好看的了,我們再來看下的一個方法吧
在這里插入圖片描述
就是判斷某個時間戳是不是在這個窗口中
時間戳要大於等於窗口開始時間 && 小於這個結束時間。

接下來再來看下這個管理窗口的類LeapArray

3.3 LeapArray

在這里插入圖片描述

看下它的成員, 窗口長度, 樣本數sampleCount 也就是窗口個數, intervalInMs ,再就是窗口數組
看看它的構造方法
在這里插入圖片描述
這個構造方法其實就是計算出來這個窗口長度,創建了窗口數組。
再來看下一個很重要的方法


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM