HyStrix替代方案限流降級框架 Sentinel 的原理和實踐

  • 2019 年 10 月 4 日
  • 筆記

作者丨童海洋(馬蜂窩大交通事業部研發工程師)

在實際業務中可能碰到這樣的情況:

場景 1

有 A 和 B 兩個服務,服務 A 作為業務訪問的入口直接暴露給用戶使用,服務 B 由 A 調用,負責查詢一部分供應商的資訊,並在設定時間內返回。這時由於出現某種原因,導致業務B的響應時間超時,需要5s才能獲取到供應商的查詢資訊。即使我們忽略其它操作的時間,這也足以帶給用戶非常差的體驗。

這類問題我們會用熔斷降級來解決,具體是指當下游服務 B 因為某種原因突然變得不可用或響應時間超過預設值時,上游的 A 服務為了保證核心服務的可用性,會直接返回用戶一個 Mock 數據,緩解伺服器的壓力。

場景 2

某個業務系統的正常 QPS 為 25, 這時突然出現了一個爬蟲程式,以 QPS 高達 100 的頻率爬取業務數據,正逐漸將業務拖垮,嚴重影響正常用戶的訪問。

對於這類明顯的異常,我們可以採用流量控制的方式,當系統 QPS 超過 20 後,直接拒絕其餘的訪問請求,來保證系統可用。

可見,在生產環境下,熔斷降級和流量控制對保證線上服務的穩定可靠起到重要作用。特別是隨著微服務的流行,服務和服務之間的穩定性變得越來越重要,熔斷降級和流量控制等策略及更好的實現手段也更受關注。在此,牆裂推薦大家使用 Sentinel 完成服務的熔斷降級和流量控制。

一、什麼是 Sentinel?

Sentinel 是面向分散式服務架構的輕量級流量控制組件,由阿里開源,主要以流量為切入點,從限流、流量整形、熔斷降級、系統負載保護等多個維度來保障微服務的穩定性。

這裡借用一張表,來說明 Sentinel 和其它主流中間件的性能對比:

從上面的對比我們可以看到,Sentinel 的優勢還是比較明顯的,比如更豐富的熔斷降級和限流策略、支援系統自適應保護、比較易用的控制台、良好的擴展性,以及更廣泛的開源生態等。

以下我將結合官方文檔和在業務實際應用過程中的理解,介紹 Sentinel 的原理、核心概念和如何使用。

二、Sentinel 原理及核心概念

2.1 核心概念

  • ResourceWrapper

Sentinel 控制的對象即為資源,調用 Entry 方法的時候會 New 一個資源對象,資源由一個全局唯一的資源名稱標識。

  • Context

Sentinel 的上下文,包含了上下文名稱,一個調用鏈一個 Context,可以顯示創建或者在調用 Entry 的時候隱式創建。

  • Node

持有運行時的資源的各種統計數據。

  1. 一個 Resource 在同一個 Context 中有且僅有一個 DefaultNode 與之對應
  2. 一個 Resource 全局有且僅有一個 ClusterNode
  • Entry

代表一次對資源的訪問,每訪問一個資源都會創建一個 Entry,在 Context 中以一個雙向鏈表存在。

必須調用 exit() 方法的原因就在於這個鏈表:exit 方法中會判斷上下文的當前 entry 是不是 this,此時其他 entry 掉用 exit 會發現不相等,從而拋出異常。

  • ProcessorSlot

處理插槽,資源的各種控制都通過不同的 Slot 實現類去完成。

  • ProcessorSlotChain

由各個處理插槽組成的鏈表,每個資源在整個服務中對應一個處理鏈。

  • Rule

用戶定義的各種規則。

  • RuleManager

載入並管理 Rule。

2.2 工作原理

在 Sentinel 里,所有的資源都對應一個資源名稱(resourceName),每次資源調用都會創建一個 Entry 對象。Entry 可以通過對主流框架的適配自動創建,也可以通過註解的方式或調用 SphU API 顯式創建。Entry 創建的時候,同時也會創建一系列功能插槽(slot chain),這些插槽有不同的職責,例如:

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

整體框架圖:

Sentinel 將 SlotChainBuilder 作為 SPI 介面進行擴展,使得 Slot Chain 具備了擴展的能力。可以自行加入自定義的 slot 並編排 slot 間的順序,來為 Sentinel 添加自定義功能:

下面重點講解流量控制和熔斷降級部分。

2.2.1 流量控制

本次介紹不涉及集群流控,因為集群流控需要通過配置單獨的 Server 與其它實例通訊,來判斷是否調用規則。而在我們現有的環境下,生產部署的集群是不區分 Client 和 Server 的。因此,以下介紹以單個實例的流量控制為主。

上文簡單提到,流量控制(Flow Control)的原理是監控應用流量的 QPS 或並發執行緒數等指標,當達到指定的閾值時對流量進行控制,以避免被瞬時的流量高峰衝垮,從而保障應用的高可用性。

同一個資源可以創建多條限流規則。Sentinel 中的 FlowSlot 會對該資源的所有限流規則依次遍歷,直到有規則觸發限流或者所有規則遍歷完畢。

  • 並發執行緒數流量控制

並發執行緒數限流用於保護業務執行緒數不被耗盡。例如,當應用所依賴的下游應用由於某種原因導致服務不穩定、響應延遲增加,對於調用者來說,意味著吞吐量下降和更多的執行緒數佔用,極端情況下甚至導致執行緒池耗盡。為應對太多執行緒佔用的情況,業內有使用隔離的方案,比如通過不同業務邏輯使用不同執行緒池來隔離業務自身之間的資源爭搶(執行緒池隔離)。這種隔離方案雖然隔離性比較好,但是代價就是執行緒數目太多,執行緒上下文切換的 overhead 比較大,特別是對低延時的調用有比較大的影響。

Sentinel 並發執行緒數限流不負責創建和管理執行緒池,而是簡單統計當前請求上下文的執行緒數目,如果超出閾值,新的請求會被立即拒絕,效果類似於訊號量隔離。

  • QPS 流量控制

當 QPS 超過某個閾值的時候,則採取措施進行流量控制。流量控制的手段包括以下幾種:直接拒絕、Warm Up、勻速排隊。對應 FlowRule 中的 controlBehavior 欄位。

  • 直接拒絕

直接拒絕(RuleConstant.CONTROL_BEHAVIOR_DEFAULT)方式是默認的流量控制方式,當 QPS 超過任意規則的閾值後,新的請求就會被立即拒絕,拒絕方式為拋出 FlowException。這種方式適用於對系統處理能力確切已知的情況下,比如通過壓測確定了系統的準確水位時。

  • Warm Up

Warm Up(RuleConstant.CONTROL_BEHAVIOR_WARM_UP)方式,即預熱/冷啟動方式。系統長期處於低水位的情況下,當流量突然增加時,直接把系統拉升到高水位可能瞬間把系統壓垮。通過「冷啟動」讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限,可以給冷系統一個預熱的時間,避免冷系統被壓垮。

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

  • 勻速排隊

勻速排隊(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式會嚴格控制請求通過的間隔時間,即讓請求以均勻的速度通過,與之對應的是漏桶演算法。

勻速排隊方式的作用如下圖所示:

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

  • 預加熱 + 勻速排隊

即 Warm Up + 勻速排隊。

  • 基於調用關係的流量控制

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

有了調用鏈路的統計資訊,我們可以衍生出多種流量控制手段。

1)根據調用方限流

ContextUtil.enter(resourceName, origin) 方法中的 origin 參數標明了調用方身份。這些資訊會在 ClusterBuilderSlot 中被統計。

流控規則中的 limitApp 欄位用於根據調用來源進行流量控制。該欄位的值有以下三種選項,分別對應不同的場景:

  • default:表示不區分調用者,來自任何調用者的請求都將進行限流統計。如果這個資源名的調用總和超過了這條規則定義的閾值,則觸發限流。
  • {some_origin_name}:表示針對特定的調用者,只有來自這個調用者的請求才會進行流量控制。例如 NodeA 配置了一條針對調用者 caller1 的規則,那麼當且僅當來自 caller1 對 NodeA 的請求才會觸發流量控制。
  • other:表示針對除 {some_origin_name} 以外的其餘調用方的流量進行流量控制。例如,資源 NodeA 配置了一條針對調用者 caller1 的限流規則,同時又配置了一條調用者為 other 的規則,那麼任意來自非 caller1 對 NodeA 的調用,都不能超過 other 這條規則定義的閾值。

同一個資源名可以配置多條規則,規則的生效順序為:{some_origin_name} > other > default。

2)根據調用鏈路入口限流:鏈路限流

NodeSelectorSlot 中記錄了資源之間的調用鏈路,這些資源通過調用關係,相互之間構成一棵調用樹。這棵樹的根節點是一個名字為 machine-root 的虛擬節點,調用鏈的入口都是這個虛節點的子節點。

一棵典型的調用樹如下圖所示:

上圖中來自入口 Entrance1 和 Entrance2 的請求都調用到了資源 NodeA,Sentinel 允許只根據某個入口的統計資訊對資源限流。比如我們可以設置 FlowRule.strategy 為 RuleConstant.CHAIN,同時設置 FlowRule.refResource 為 Entrance1 來表示只有從入口 Entrance1 的調用才會記錄到 NodeA 的限流統計當中,而不關心經 Entrance2 到來的調用。

調用鏈的入口(上下文)是通過 API 方法 ContextUtil.enter(contextName) 定義的,其中 contextName 即對應調用鏈路入口名稱。

  • 具有關係的資源流量控制:關聯流量控制

當兩個資源之間具有資源爭搶或者依賴關係的時候,這兩個資源便具有了關聯。比如對資料庫同一個欄位的讀操作和寫操作存在爭搶,讀的速度過高會影響寫的速度,寫的速度過高會影響讀的速度。如果放任讀寫操作爭搶資源,則爭搶本身帶來的開銷會降低整體的吞吐量。

這裡可使用關聯限流來避免具有關聯關係的資源之間過度爭搶。舉例來說,read_db 和 write_db 這兩個資源分別代表資料庫讀寫,我們可以給 read_db 設置限流規則來達到寫優先的目的:設置FlowRule.strategy為RuleConstant.RELATE同時設置FlowRule.refResource為write_db。這樣當寫庫操作過於頻繁時,讀數據的請求會被限流。

規則判斷流程:

2.2.2 熔斷降級

Sentinel 目前有三種降級策略。當然, 我們還可以通過自定義 Slot 和 Rule 的方式制定符合要求的降級策略。

  • 平均響應時間 (DEGRADE_GRADE_RT):當 1s 內持續進入 5 個請求,對應時刻的平均響應時間(秒級)均超過閾值(count,以 ms 為單位),那麼在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地熔斷(拋出 DegradeException)。注意 Sentinel 默認統計的 RT 上限是 4900 ms,超出此閾值的都會算作 4900 ms,若需要變更此上限可以通過啟動配置項-Dcsp.sentinel.statistic.max.rt=xxx 來配置。
  • 異常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):當資源的每秒請求量 >= 5,並且每秒異常總數占通過量的比值超過閾值(DegradeRule 中的 count)之後,資源進入降級狀態,即在接下的時間窗口(DegradeRule 中的 timeWindow,以 s 為單位)之內,對這個方法的調用都會自動地返回。異常比率的閾值範圍是 [0.0, 1.0],代表 0% – 100%。
  • 異常數 (DEGRADE_GRADE_EXCEPTION_COUNT):當資源近 1 分鐘的異常數目超過閾值之後會進行熔斷。注意由於統計時間窗口是分鐘級別的,若 timeWindow 小於 60s,則結束熔斷狀態後仍可能再進入熔斷狀態。

規則判斷流程:

當 SlotChain 執行到 DegradeSlot 時,DegradeSlot 先調用 DegradeManager 判斷規則。執行完畢後,如果還有下一個 Slot,就執行下一個 Slot。

DegradeManager 會調用 DegradeRule 進行判斷。DegradeRule 在判斷符合熔斷規則的情況下會拋出異常,否則資源正常執行。

三、Sentinel 使用示例

3.1 Sentinel 啟動配置項

  • sentinel-core 配置項
  • sentinel-transport-common 配置項:

3.2 編碼方法 (調用SphU API顯示創建)

public ResultWrapper<OrderDTO> getOrder(String id) {          /**           * pre inception           */          Entry entry = null;          try {              entry = SphU.entry("getOrder");              ResultWrapper<OrderDTO> orderResultWrapper = testRestManagerImpl.getOrderInfo(id);                if (orderResultWrapper.getData() == null) {                  orderResultWrapper.setData(new OrderDTO("No Order", "No Shop", " Random Data"));              }                return orderResultWrapper;          } catch (BlockException e) {              Tracer.trace(e);              log.info(id + " 服務熔斷");              /**               * 可以mock一個假數據返回               */              return new ResultWrapper<>();          } catch (Exception e) {              log.info(id + " 業務異常");              /**               * 業務異常,mock假數據返回               */              return new ResultWrapper<>();          } finally {              if (entry != null) {                  entry.exit();              }          }          /**           * 之前也可以不返回結果,這裡調用其他的業務邏輯           * post inception           */      }  

由實例程式碼看出,編碼方式對系統是有一定入侵的。但好處也很明顯,就是資源真正由編碼人員掌控,資源甚至可以是一個小小的程式碼塊。

3.3 註解方式

核心註解:@SentinelResource

  • value:資源名稱,必需項(不能為空)
  • entryType:entry 類型,可選項(默認為 EntryType.OUT)
  • blockHandler / blockHandlerClass: blockHandler 對應處理 BlockException 的函數名稱,可選項。blockHandler 函數訪問範圍需要是 public,返回類型需要與原方法相匹配,參數類型需要和原方法相匹配並且最後加一個額外的參數,類型為 BlockException。blockHandler 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 blockHandlerClass 為對應的類的 Class 對象,注意對應的函數必須為 static 函數,否則無法解析。
  • fallback:fallback 函數名稱,可選項,用於在拋出異常的時候提供 fallback 處理邏輯。fallback 函數可以針對所有類型的異常(除了 exceptionsToIgnore 裡面排除掉的異常類型)進行處理。fallback 函數簽名和位置要求:
  • 返回值類型必須與原函數返回值類型一致;
  • 方法參數列表需要和原函數一致,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。
  • fallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 為對應的類的 Class 對象,注意對應的函數必須為 static 函數,否則無法解析。
  • defaultFallback(since 1.6.0):默認的 fallback 函數名稱,可選項,通常用於通用的 fallback 邏輯(即可以用於很多服務或方法)。默認 fallback 函數可以針對所有類型的異常(除了 exceptionsToIgnore 裡面排除掉的異常類型)進行處理。若同時配置了 fallback 和 defaultFallback,則只有 fallback 會生效。defaultFallback 函數簽名要求:
  • 返回值類型必須與原函數返回值類型一致;
  • 方法參數列表需要為空,或者可以額外多一個 Throwable 類型的參數用於接收對應的異常。
  • defaultFallback 函數默認需要和原方法在同一個類中。若希望使用其他類的函數,則可以指定 fallbackClass 為對應的類的 Class 對象,注意對應的函數必須為 static 函數,否則無法解析。
  • exceptionsToIgnore(since1.6.0):用於指定哪些異常被排除掉,不會計入異常統計中,也不會進入 fallback 邏輯中,而是會原樣拋出。
首先,注入SentinelResource註解  @Configuration  public class SentinelAspectConfiguration {        @Bean      public SentinelResourceAspect sentinelResourceAspect() {          return new SentinelResourceAspect();      }  }    然後直接使用註解:  @SentinelResource(value = "getOrder",entryType = EntryType.IN, fallback = "getOrderFallBack", fallbackClass = {ITestRestAPIUtil.class}, blockHandler = "getOrderHandler", blockHandlerClass = {ITestRestAPIUtil.class})  public ResultWrapper<OrderDTO> getOrder(String id) {      ResultWrapper<OrderDTO> orderResultWrapper = testRestManagerImpl.getOrderInfo(id);        if (orderResultWrapper.getData() == null) {          orderResultWrapper.setData(new OrderDTO("No Order", "No Shop", " Random Data"));      }        return orderResultWrapper;  }    然後實現我們設定的BlockHander和fallback方法  @Slf4j  public class ITestRestAPIUtil {      public static ResultWrapper<OrderDTO> getOrderHandler(String id, BlockException ex) {  //        log.info(JSON.toJSONString(ex));          //這裡可以進行埋點          //mock假數據返回          return new ResultWrapper().setSuccess(true).setData(new OrderDTO()).setCode("服務已被降級").addMessage("服務已被降級");      }        public static ResultWrapper<OrderDTO> getOrderFallBack(String id, Throwable throwable) {          log.info("捕捉到業務異常:{}", throwable.toString());  //        log.error("getOrderError", id);          //mock假數據返回          return new ResultWrapper<OrderDTO>().setSuccess(false).addMessage("捕捉到業務異常");         }  }  

由實例程式碼可以看出,註解方式對系統侵入較小,利於維護與修改。但由於 SentinelResource 僅作用於 Method,資源和方法的界限在這裡混淆。

3.4 動態配置規則

如果規則的修改無法實時改變,那Sentinel的效果將大打折扣。我們可以配合 Apollo 來動態的修改所需規則。

官方給出了 sentinel-datasource-apollo 的依賴來支援 Apollo 的實現,個人認為實用意義不大。因為項目中可能不止 Sentinel 會用到 Apollo,沒有理由將 Sentinel 和其它業務使用完全剝離開來。而且該依賴也只是將 Apollo 的使用封裝了起來。

@Configuration  public class SentinelConfig {        @Resource      private ApolloConfigureCenter apolloConfigureCenter;        @PostConstruct      private void initFlowRule() {          List<FlowRule> rules = JSON.parseArray(apolloConfigureCenter.getFlowControlRule(), FlowRule.class);          FlowRuleManager.loadRules(rules);      }        @PostConstruct      private void initDegradeRule() {          List<DegradeRule> rules = JSON.parseArray(apolloConfigureCenter.getDegradeRule(), DegradeRule.class);          DegradeRuleManager.loadRules(rules);      }        //自定義規則,另一種寫在程式碼中方法,也可以配置到apollo中      @PostConstruct      private void initCustomRule() {          List<CustomRule> rules = new ArrayList<>();          CustomRule customRule = new CustomRule();          customRule.setPercentage();          customRule.setTimeWindow();          customRule.setResource("testCustom");          rules.add(customRule);          CustomRuleManager.loadRules(rules);      }  }  

可以看出,上面的實例是通過 loadRules 這個方法來注入規則的。分析源碼可以得知,loadRules 其實是對之前規則的一次重製,這就意味著我們可以動態的修改規則,只需要對 Apollo 進行監聽就可以了。

其它規則配置大致與此思路類似,都是 Clear 原先的 Rules,然後添加新的 Rules。

3.5 實時監控使用方式

下載 Sentinel 源碼,打包運行 sentinel-dashboard.jar 就行了。

java -Dserver.port=8082 -Dcsp.sentinel.dashboard.server=localhost:8082 -jar sentinel-dashboard/target/sentinel-dashboard.jar 可以指定實時監控頁面的埠號與 IP 地址。

四、Sentinel 進階使用

自定義 Rule 與自定義 SlotChain

Sentinel已經實現的功能基本可以滿足大部分情況下的需求。但在一些特殊場景下,我們還是需要設定自己的規則。

這裡以通過 Sentinel 實現灰度作為示例。

首先自定義一個CustomRule:

public class CustomRule extends AbstractRule {      private static final int RT_MAX_EXCEED_N = ;      private double percentage;      private int timeWindow;      private final AtomicBoolean cut = new AtomicBoolean(false);      private AtomicLong passCount = new AtomicLong(0L);        public CustomRule() {      }        public CustomRule(String resourceName) {          this.setResource(resourceName);      }        public double getPercentage() {          return this.percentage;      }        public CustomRule setPercentage(double percentage) {          this.percentage = percentage;          return this;      }        public int getTimeWindow() {          return timeWindow;      }        public CustomRule setTimeWindow(int timeWindow) {          this.timeWindow = timeWindow;          return this;      }        private boolean isCut() {          return this.cut.get();      }        private void setCut(boolean cut) {          this.cut.set(cut);      }        public AtomicLong getPassCount() {          return this.passCount;      }        public boolean equals(Object o) {          if (this == o) {              return true;          } else if (!(o instanceof CustomRule)) {              return false;          } else if (!super.equals(o)) {              return false;          } else {              CustomRule that = (CustomRule)o;              if (this.percentage != that.percentage) {                  return false;              } else {                  return false;              }          }      }        public int hashCode() {          int result = super.hashCode();          result =  * result + (new Double(this.percentage)).hashCode();          result =  * result + this.timeWindow;          return result;      }        public boolean passCheck(Context context, DefaultNode node, int acquireCount, Object... args) {          //實際情況下肯定不會只傳一個參數,需要看情況自己解析          Object[] objects = args;          String uid = (String)objects[];          if (StringUtils.isBlank(uid)) {              return true;          } else {              取後兩位與灰度值進行比較              double greyUID = Double.parseDouble(uid.substring(uid.length()-));              if (greyUID < this.percentage) {                  return true;              }          }            return false;      }        public String toString() {          return "CustomRule{resource=" + this.getResource() + ", percentage=" + this.percentage + ", limitApp=" + this.getLimitApp() + ", timeWindow=" + this.timeWindow + "}";      }        private static final class ResetTask implements Runnable {          private CustomRule rule;            ResetTask(CustomRule rule) {              this.rule = rule;          }            public void run() {              this.rule.getPassCount().set(0L);              this.rule.cut.set(false);          }      }  }  

定義 CustomRuleException:

public class CustomException extends BlockException {      public CustomException(String ruleLimitApp) {          super(ruleLimitApp);      }        public CustomException(String ruleLimitApp, CustomRule rule) {          super(ruleLimitApp, rule);      }        public CustomException(String message, Throwable cause) {          super(message, cause);      }        public CustomException(String ruleLimitApp, String message) {          super(ruleLimitApp, message);      }        @Override      public Throwable fillInStackTrace() {          return this;      }        /**       * Get triggered rule.       * Note: the rule result is a reference to rule map and SHOULD NOT be modified.       *       * @return triggered rule       * @since 1.4.2       */      @Override      public CustomRule getRule() {          return rule.as(CustomRule.class);      }  }  

定義 CustomRuleManager:

public class CustomRuleManager {        private static final Map<String, Set<CustomRule>> greyRules = new ConcurrentHashMap<>();        private static final RulePropertyListener LISTENER = new RulePropertyListener();      private static SentinelProperty<List<CustomRule>> currentProperty              = new DynamicSentinelProperty<>();        static {          currentProperty.addListener(LISTENER);      }        public static void register2Property(SentinelProperty<List<CustomRule>> property) {          AssertUtil.notNull(property, "property cannot be null");          synchronized (LISTENER) {              RecordLog.info("[CustomRuleManager] Registering new property to degrade rule manager");              currentProperty.removeListener(LISTENER);              property.addListener(LISTENER);              currentProperty = property;          }      }        public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count, Object ...args)              throws BlockException {            Set<CustomRule> rules = greyRules.get(resource.getName());          if (rules == null) {              return;          }            for (CustomRule rule : rules) {              if (!rule.passCheck(context, node, count, args)) {                  throw new CustomException(rule.getLimitApp(), rule);              }          }      }        public static boolean hasConfig(String resource) {          if (resource == null) {              return false;          }          return greyRules.containsKey(resource);      }        /**       * Get a copy of the rules.       *       * @return a new copy of the rules.       */      public static List<CustomRule> getRules() {          List<CustomRule> rules = new ArrayList<>();          for (Map.Entry<String, Set<CustomRule>> entry : greyRules.entrySet()) {              rules.addAll(entry.getValue());          }          return rules;      }        /**       * Load {@link CustomRule}s, former rules will be replaced.       *       * @param rules new rules to load.       */      public static void loadRules(List<CustomRule> rules) {          try {              currentProperty.updateValue(rules);          } catch (Throwable e) {              RecordLog.warn("[CustomRuleManager] Unexpected error when loading degrade rules", e);          }      }        /**       * Set degrade rules for provided resource. Former rules of the resource will be replaced.       *       * @param resourceName valid resource name       * @param rules        new rule set to load       * @return whether the rules has actually been updated       * @since 1.5.0       */      public static boolean setRulesForResource(String resourceName, Set<CustomRule> rules) {          AssertUtil.notEmpty(resourceName, "resourceName cannot be empty");          try {              Map<String, Set<CustomRule>> newRuleMap = new HashMap<>(greyRules);              if (rules == null) {                  newRuleMap.remove(resourceName);              } else {                  Set<CustomRule> newSet = new HashSet<>();                  for (CustomRule rule : rules) {                      if (isValidRule(rule) && resourceName.equals(rule.getResource())) {                          newSet.add(rule);                      }                  }                  newRuleMap.put(resourceName, newSet);              }              List<CustomRule> allRules = new ArrayList<>();              for (Set<CustomRule> set : newRuleMap.values()) {                  allRules.addAll(set);              }              return currentProperty.updateValue(allRules);          } catch (Throwable e) {              RecordLog.warn(                      "[CustomRuleManager] Unexpected error when setting degrade rules for resource: " + resourceName, e);              return false;          }      }        private static class RulePropertyListener implements PropertyListener<List<CustomRule>> {            @Override          public void configUpdate(List<CustomRule> conf) {              Map<String, Set<CustomRule>> rules = loadDegradeConf(conf);              if (rules != null) {                  greyRules.clear();                  greyRules.putAll(rules);              }              RecordLog.info("[CustomRuleManager] Degrade rules received: " + greyRules);          }            @Override          public void configLoad(List<CustomRule> conf) {              Map<String, Set<CustomRule>> rules = loadDegradeConf(conf);              if (rules != null) {                  greyRules.clear();                  greyRules.putAll(rules);              }              RecordLog.info("[CustomRuleManager] Degrade rules loaded: " + greyRules);          }            private Map<String, Set<CustomRule>> loadDegradeConf(List<CustomRule> list) {              Map<String, Set<CustomRule>> newRuleMap = new ConcurrentHashMap<>();                if (list == null || list.isEmpty()) {                  return newRuleMap;              }                for (CustomRule rule : list) {                  if (!isValidRule(rule)) {                      RecordLog.warn(                              "[CustomRuleManager] Ignoring invalid degrade rule when loading new rules: " + rule);                      continue;                  }                    if (StringUtil.isBlank(rule.getLimitApp())) {                      rule.setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);                  }                    String identity = rule.getResource();                  Set<CustomRule> ruleSet = newRuleMap.get(identity);                  if (ruleSet == null) {                      ruleSet = new HashSet<>();                      newRuleMap.put(identity, ruleSet);                  }                  ruleSet.add(rule);              }                return newRuleMap;          }      }        public static boolean isValidRule(CustomRule rule) {          boolean baseValid = rule != null && !StringUtil.isBlank(rule.getResource())                  && rule.getPercentage() >=  && rule.getTimeWindow() > ;          if (!baseValid) {              return false;          }          // Warn for RT mode that exceeds the {@code TIME_DROP_VALVE}.          int maxAllowedRt = Constants.TIME_DROP_VALVE;          return true;      }  }  

自定義 CustomSlot:

public class CustomSlot extends AbstractLinkedProcessorSlot<DefaultNode> {      public CustomSlot() {        }        @Override      public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable {          CustomRuleManager.checkDegrade(resourceWrapper, context, node, count, args);          fireEntry(context, resourceWrapper, node, count, prioritized, args);      }        @Override      public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {          fireExit(context, resourceWrapper, count, args);      }  }  

自定義 SlotChain:

public class CustomSlotChainBuilder implements SlotChainBuilder {        @Override      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 CustomSlot());          chain.addLast(new FlowSlot());          chain.addLast(new DegradeSlot());            return chain;      }  }  

這裡將灰度置於流控與熔斷降級之前:

測試:

最後,關於 Sentinel 源碼解讀後面有機會再和大家分享!

參考文檔:

https://github.com/alibaba/Sentinel/wiki/%E4%B8%BB%E9%A1%B5