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


目錄:小圈的 springCloud 高併發系列

推薦閱讀
nacos 實戰(史上最全)
sentinel (史上最全+入門教程)
springcloud + webflux 高併發實戰
Webflux(史上最全)
SpringCloud gateway (史上最全)
和 1000+ Java 高併發 發燒友、 一塊兒 交流 、學習、入大廠、作架構,GO

sentinel 基本概念

開發的緣由,須要對吞吐量(TPS)、QPS、併發數、響應時間(RT)幾個概念作下了解,查自百度百科,記錄以下:node

  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,只是應用於特定場景的吞吐量)

一、什麼是Sentinel:

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

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

Sentinel 具備如下特徵:

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

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

Sentinel的生態圈算法

img

Sentinel主要特性:

img

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

到這已經學習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的職責比較簡單,主要作了兩件事:

1、爲每一個資源建立一個clusterNode,而後把clusterNode塞到DefaultNode中去

2、將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各司其職,能夠分爲如下幾種類型:

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

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

3、進行系統保護,限流,降級等規則校驗的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 ,再就是窗口數組
看看它的構造方法
在這裏插入圖片描述 這個構造方法其實就是計算出來這個窗口長度,建立了窗口數組。 再來看下一個很重要的方法

相關文章
相關標籤/搜索