Sentinel為什麼這麼強,我扒了扒背後的實現原理

2023-04-25 15:02:32

大家好,我是三友~~

最近我在整理程式碼倉庫的時候突然發現了被塵封了接近兩年之久的Sentinel原始碼庫

兩年前我出於好奇心扒了一下Sentinel的原始碼,但是由於Sentinel本身原始碼並不複雜,在簡單扒了扒之後幾乎就再沒扒過了

那麼既然現在又讓我看到了,所以我準備再來好好地扒一扒,然後順帶寫篇文章來總結一下。

Sentinel簡介

Sentinel是阿里開源的一款面向分散式、多語言異構化服務架構的流量治理元件。

主要以流量為切入點,從流量路由、流量控制、流量整形、熔斷降級、系統自適應過載保護、熱點流量防護等多個維度來幫助開發者保障微服務的穩定性。

上面兩句話來自Sentinel官網的自我介紹,從這短短的兩句話就可以看出Sentinel的定位和強大的功能。

核心概念

要想理解一個新的技術,那麼首先你得理解它的一些核心概念

資源

資源是Sentinel中一個非常重要的概念,資源就是Sentinel所保護的物件。

資源可以是一段程式碼,又或者是一個介面,Sentinel中並沒有什麼強制規定,但是實際專案中一般以一個介面為一個資源,比如說一個http介面,又或者是rpc介面,它們就是資源,可以被保護。

資源是通過Sentinel的API定義的,每個資源都有一個對應的名稱,比如對於一個http介面資源來說,Sentinel預設的資源名稱就是請求路徑。

規則

規則也是一個重要的概念,規則其實比較好理解,比如說要對一個資源進行限流,那麼限流的條件就是規則,後面在限流的時候會基於這個規則來判定是否需要限流。

Sentinel的規則分為流量控制規則、熔斷降級規則以及系統保護規則,不同的規則實現的效果不一樣。

來個Demo

為了兼顧文章的完整性和我一貫的風格,必須要來個demo,如果你已經使用過了Sentinel,那麼就可以直接pass這一節,直接快進到核心原理。

1、基本使用

引入依賴

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.8.6</version>
</dependency>

測試程式碼

public class SentinelSimpleDemo {

    public static void main(String[] args) {
        //載入流控規則
        initFlowRules();

        for (int i = 0; i < 5; i++) {
            Entry entry = null;
            try {
                entry = SphU.entry("sayHello");
                //被保護的邏輯
                System.out.println("存取sayHello資源");
            } catch (BlockException ex) {
                System.out.println("被流量控制了,可以進行降級處理");
            } finally {
                if (entry != null) {
                    entry.exit();
                }
            }
        }
    }

    private static void initFlowRules() {
        List<FlowRule> rules = new ArrayList<>();

        //建立一個流控規則
        FlowRule rule = new FlowRule();
        //對sayHello這個資源限流
        rule.setResource("sayHello");
        //基於qps限流
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //qps最大為2,超過2就要被限流
        rule.setCount(2);

        rules.add(rule);

        //設定規則
        FlowRuleManager.loadRules(rules);
    }

}

解釋一下上面這段程式碼的意思

  • initFlowRules方法就是載入一個限流的規則,這個規則作用於sayHello這個資源,基於qps限流,當qps超過2之後就會觸發限流。

  • SphU.entry("sayHello")這行程式碼是Sentinel最最核心的原始碼,這行程式碼錶面看似風平浪靜,實則暗流湧動。這行程式碼錶明接下來需要存取某個資源(引數就是資源名稱),會去檢查需要被存取的資源是否達到設定的流控、熔斷等規則。對於demo來說,就是檢查sayHello這個資源是否達到了設定的流量控制規則。

  • catch BlockException也很重要,當丟擲BlockException這個異常,說明觸發了一些設定的保護規則,比如限流了,這裡面就可以進行降級操作。

  • System.out.println("存取sayHello資源")這行程式碼錶面是一個列印語句,實則就是前面一直在說的需要被保護的資源。

所以上面這段程式碼的整體意思就是對sayHello這個需要存取的資源設定了一個流控規則,規則的內容是當qps到達2的時候觸發限流,之後迴圈5次存取sayHello這個資源,在存取之前通過SphU.entry("sayHello")這行程式碼進行限流規則的檢查,如果達到了限流的規則的條件,會丟擲BlockException。

測試結果

從結果可以看出,當前兩次存取sayHello成功之後,qps達到了2,之後再存取就被限流了,失敗了。

2、整合Spring

在實際的專案使用中一般不會直接寫上面的那段demo程式碼,而是整合到Spring環境底下。

引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2.2.5.RELEASE</version>
</dependency>

之後提供一個/sayHello介面

@RestController
public class SentinelDemoController {

    @GetMapping("/sayHello")
    public String sayHello() throws InterruptedException {
        return "hello";
    }

}

組態檔

server:
  port: 9527
  
spring:
  application:
    name: SentinelDemo

到這demo就搭建完成了。

此時你心理肯定有疑問,那前面提到的資源和對應的規則去哪了?

前面在說資源概念的時候,我提到Sentinel中預設一個http介面就是一個資源,並且資源的名稱就是介面的請求路徑。

而真正的原因是Sentinel實現了SpringMVC中的HandlerInterceptor介面,在呼叫Controller介面之前,會將一個呼叫介面設定為一個資源,程式碼如下

getResourceName方法就是獲取資源名,其實就是介面的請求路徑,比如前面提供的介面路徑是/sayHello,那麼資源名就是/sayHello

再後面的程式碼就是呼叫上面demo中提到表面風平浪靜,實則暗流湧動的SphU.entry(..)方法,檢查被呼叫的資源是否達到了設定的規則。

好了,既然資源預設是介面,已經有了,那麼規則呢?

規則當然可以按照第一個demo的方式來做,比如在Controller介面中載入,程式碼如下。

@RestController
public class SentinelDemoController {

    static {
        List<FlowRule> rules = new ArrayList<>();

        //建立一個流控規則
        FlowRule rule = new FlowRule();
        //對/sayHello這個資源限流
        rule.setResource("/sayHello");
        //基於qps限流
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        //qps最大為2,超過2就要被限流
        rule.setCount(2);

        rules.add(rule);

        //設定規則
        FlowRuleManager.loadRules(rules);
    }

    @GetMapping("/sayHello")
    public String sayHello() throws InterruptedException {
        return "hello";
    }

}

此時啟動專案,在瀏覽器輸入以下連結

http://localhost:9527/sayHello

瘋狂快速使勁地多點幾次,就出現下面這種情況

可以看出規則生效了,介面被Sentinel限流了,至於為什麼出現這個提示,是因為Sentinel有預設的處理BlockException的機制,就在前面提到的進入資源的後面。

當然,你也可以自定義處理的邏輯,實現BlockExceptionHandler介面就可以了。

雖然上面這種寫死規則的方式可以使用,但是在實際的專案中,肯定希望能夠基於系統當期那執行的狀態來動態調整規則,所以Sentinel提供了一個叫Dashboard應用的控制檯,可以通過控制檯來動態修改規則。

控制檯其實就是一個jar包,可以從Sentinel的github倉庫上下載,或者是通過從下面這個地址獲取。

連結:https://pan.baidu.com/s/1Lw8V5ab_FUq934nLWDjfaw 提取碼:obr5

之後通過java -jar命令啟動就可以了,埠預設8080,瀏覽器存取http://ip:8080/#/login就可以登入控制檯了,使用者名稱和密碼預設都是sentinel。

此時服務要接入控制檯,只需要在組態檔上加上控制檯的ip和埠即可

spring:
  cloud:
    sentinel:
      transport:
        # 指定控制檯的ip和埠
        dashboard: localhost:8080

專案剛啟動的時候控制檯預設是沒有資料的,需要存取一下介面,之後就有了。

之後就可以看到/sayHello這個資源,後面就可以通過頁面設定規則。

核心原理

講完demo,接下來就來講一講Sentinel的核心原理,也就是前面提到暗流湧動的SphU.entry(..)這行程式碼背後的邏輯。

Sentinel會為每個資源建立一個處理鏈條,就是一個責任鏈,第一次存取這個資源的時候建立,之後就一直複用,所以這個處理鏈條每個資源有且只有一個。

SphU.entry(..)這行程式碼背後就會呼叫責任鏈來完成對資源的檢查邏輯。

這個責任鏈條中每個處理節點被稱為ProcessorSlot,中文意思就是處理器槽

這個ProcessorSlot有很多實現,但是Sentinel的核心就下面這8個:

  • NodeSelectorSlot
  • ClusterBuilderSlot
  • LogSlot
  • StatisticSlot
  • AuthoritySlot
  • SystemSlot
  • FlowSlot
  • DegradeSlot

這些實現會通過SPI機制載入,然後按照一定的順序組成一個責任鏈。

預設情況下,節點是按照如下的順序進行排序的

雖然預設就8個,但是如果你想擴充套件,只要實現ProcessorSlot,按照SPI的規定設定一下就行。

下面就來按照上面節點的處理順序來好好扒一扒這8個ProcessorSlot

1、NodeSelectorSlot

這個節點的作用是來設定當前資源對應的入口統計Node

首先什麼是統計Node?

比如就拿上面的例子來說,當/sayHello這個資源的qps超過2的時候,要觸發限流。

但是有個疑問,Sentinel是怎麼知道/sayHello這個資源的qps是否達到2呢?

當然是需要進行資料統計的,只有通過統計,才知道qps是否達到2,這個進行資料統計的類在Sentinel中叫做Node。

通過Node這個統計的類就知道有多少請求,成功多少個,失敗多少個,qps是多少之類的。底層其實是使用到了滑動視窗演演算法。

那麼什麼叫對應的入口?

在Sentinel中,支援同一個資源有不同的存取入口。

舉個例子,這個例子後面會反覆提到。

假設把杭州看做是服務,西湖看做是一個資源,到達西湖有兩種方式,地鐵和公交。

所以要想存取西湖這個資源,就可以通過公交和地鐵兩種方式,而公交和地鐵就對應前面說的入口的意思。

只不過一般一個資源就一個入口,比如一個http介面一般只能通過http存取,但是Sentinel支援多入口,你可以不用,但是Sentinel有。

所以NodeSelectorSlot的作用就是選擇資源在當前呼叫入口的統計Node,這樣就實現了統計同一個資源在不同入口存取資料,用上面的例子解釋,就可以實現分別統計通過公交和地鐵存取西湖的人數。

資源的入口可以在進入資源之前通過ContextUtil.enter("入口名", origin)來指定,如果不指定,那麼入口名稱預設就是sentinel_default_context

在SpringMVC環境底下,所有的http介面資源,預設的入口都是sentinel_spring_web_context

入口名稱也可以通過控制檯看到

那麼為什麼要搞一個入口的概念呢?這裡咱先留個懸念,後面再說。

2、ClusterBuilderSlot

ClusterBuilderSlot的作用跟NodeSelectorSlot其實是差不多的,也是用來選擇統計Node,但是選擇的Node的統計維護跟NodeSelectorSlot不一樣。

ClusterBuilderSlot會選擇兩個統計Node:

  • 第一個統計Node是資源的所有入口的統計資料之和,就是資源存取的總資料

  • 第二個統計Node就是統計資源呼叫者對資源存取資料

資源呼叫者很好理解,比如一個http介面資源肯定會被呼叫,那麼呼叫這個介面的服務或者應用其實就是資源的呼叫者,但是一般資源的呼叫者就是指某個服務,後面呼叫者我可能會以服務來代替。

一個介面可以被很多服務呼叫,所以一個資源可以很多呼叫者,而不同呼叫者都會有單獨的一個統計Node,用來分別統計不同呼叫者對資源的存取資料。

舉個例子,現在存取西湖這個資源的大兄弟來自上海,那麼就會為上海建立一個統計Node,用來統計所有來自上海的人數,如果是北京,那麼就會為北京建立一個統計Node。

那麼如何知道存取資源來自哪個服務(呼叫者)呢?

也是通過ContextUtil.enter("入口名", origin)來指定,這個方法的第二個引數origin就是代表服務名的意思,預設是空。

所以ContextUtil.enter(..)可以同時指定資源的入口和呼叫者,一個資源一定有入口,因為不指定入口預設就是sentinel_default_context,但是呼叫者不指定就會沒有。

對於一個http請求來說,Sentinel預設服務名需要放到S-user這個請求頭中,所以如果你想知道介面的呼叫服務,需要在呼叫方傳送請求的時候將服務名設定到S-user請求頭中。

當資源所在的服務接收到請求時,Sentinel就會從S-user請求頭獲取到服務名,之後再通過ContextUtil.enter("入口名", "呼叫者名")來設定當前資源的呼叫者

這裡我原以為Sentinel會適配比如OpenFeign之類的框架,會自動將服務名攜帶到請求頭中,但是我翻了一下原始碼,發現並沒有去適配,不知道是出於什麼情況的考慮。

所以這一節加上上一節,我們知道了一個資源其實有三種維度的統計Node:

  • 分別統計不同入口的存取資料
  • 統計所有入口存取資料之和
  • 分別統計來自某個服務的存取資料

為了方便區分,我來給這三個統計Node取個響亮的名字

不同入口的存取資料就叫他DefaultNode,統計所有入口存取資料之和就叫他ClusterNode,來自某個服務的存取資料就叫他OriginNode。

是不是夠響亮!

那麼他們的關係就可以用下面這個圖來表示

3、LogSlot

這個Slot沒什麼好說的,通過名字可以看出來,其實就是用來列印紀錄檔的。

當發生異常,就會列印紀錄檔。

4、StatisticSlot

這個Slot就比較重要了,就是用來統計資料的。

前面說的NodeSelectorSlot和ClusterBuilderSlot,他們的作用就是根據資源當前的入口和呼叫來源來選擇對應的統計Node。

而StatisticSlot就是對這些統計Node進行實際的統計,比如加一下資源的存取執行緒數,資源的請求數量等等。

前幾個Slot其實都是準備、統計的作用,並沒有涉及限流降級之類的,他們是為限流降級提供資料支援的。

5、AuthoritySlot

Authority是授權的意思,這個Slot的作用是對資源呼叫者進行授權,就是黑白名單控制。

可以通過控制檯來新增授權規則。

在AuthoritySlot中會去獲取資源的呼叫者,之後會跟授權規則中的資源應用這個選項進行匹配,之後就會出現有以下2種情況:

  • 授權型別是黑名單,匹配上了,說明在黑名單內,那麼這個服務就不能存取這個資源,沒匹配上就可以存取

  • 授權型別是白名單。匹配上了,說明在白名單內,那麼這個服務就可以存取這個資源,沒匹配上就不可以存取

6、SystemSlot

這個的作用是根據整個系統執行的統計資料來限流的,防止當前系統負載過高。

它支援入口qps、執行緒數、響應時間、cpu使用率、負載5個限流的維度。

對於系統的入口qps、執行緒數、平均響應時間這些指標,也會有一個統計Node專門去統計,所以這個統計Node的作用就好比會去統計所有存取西湖的人數,統計也在StatisticSlot程式碼中,前面說的時候我把程式碼隱藏了

至於cpu使用率、負載指標,Sentinel會啟動一個定時任務,每隔1s會去讀取一次當前系統的cpu和負載。

7、FlowSlot

這個Slot會根據預設的規則,結合前面的統計出來的實時資訊進行流量控制。

在說FlowSlot之前,先來用之前畫的那張圖回顧一下一個資源的三種統計維度

這裡默默地注視10s。。

限流規則設定項比較多

這裡我們來好好扒一扒這些設定項的意思。

針對來源,來源就是前面說的呼叫方,這個設定表明,這個規則適用於哪個呼叫方,預設是default,就是指規則適用於所有呼叫方,如果指定了呼叫方,那麼這個規則僅僅對指定的呼叫方生效。

舉個例子來說,比如說現在想限制來自上海的存取的人數,那麼針對來源可以填上海,之後當存取的大兄弟來自上海的時候,Sentinel就會根據上海對應的OriginNode資料來判斷是否達到限流的條件。

閾值型別,就是限流條件,當資源的qps或者存取的執行緒數到達設定的單機閾值,就會觸發限流。

是否叢集,這個作用是用來對叢集控制的,因為一個服務可能在很多臺機器上,而這個的作用就是將整個叢集看成一個整體來限流,這裡就不做深入討論。

流控模式,這個流控模式的選項僅僅對閾值型別為qps有效,當閾值型別執行緒數時無效。

這個設定就比較有意思了,分為直接、關聯、鏈路三種模式。

直接模式的意思就是當資源的ClusterNode統計資料統計達到了閾值,就會觸發限流。

比如,當通過地鐵和公交存取西湖人數之和達到單機閾值之後就會觸發限流。

關聯模式下需要填寫關聯的資源名稱

關聯的意思就是當關聯資源的ClusterNode統計的qps達到了設定的閾值時,就會觸發當前資源的限流操作。

比如,假設現在西湖這個資源關聯了雷峰塔這個資源,那麼當存取雷峰塔的人數達到了指定的閾值之後,此時就觸發西湖這個資源的限流,就是雷峰塔流量高了但是限流的是西湖。

鏈路模式也一樣,它需要關聯一個入口資源

關聯入口的意思就是指,當存取資源的實際入口跟關聯入口是一樣的時候,就會根據這個入口對應的DefaultNode的統計資料來判斷是否需要限流。

也就是可以單獨限制通過公交和地鐵的存取的人數的意思。

到這,其實前面說到的一個資源的三種統計維度的資料都用到了,現在應該明白了為什麼需要這麼多維度的資料,就是為不同維度限流準備的。

最後一個設定項,流控效果,這個就是如果是通過qps來限流,並且達到了限流的條件之後會做什麼,如果是執行緒數,就直接丟擲BlockException異常

也有三種方式,快速失敗、Warm Up、排隊等待

快速失敗的意思就是指一旦觸發限流了,那麼直接丟擲BlockException異常

Warm Up的作用就是為了防止系統流量突然增加時出現瞬間把系統壓垮的情況。通過"冷啟動",讓通過的流量緩慢增加,在一定時間內逐漸增加到閾值上限。

排隊等待,很好理解,意思當出現限流了,不是拋異常,而是去排隊等待一定時間,其實就是讓請求均勻速度通過,內部使用的是傳說中的漏桶演演算法。

DegradeSlot

這是整個責任鏈中最後一個slot,這個slot的作用是用來熔斷降級的。

Sentinel支援三種熔斷策略:慢呼叫比例、異常比例 、異常數,通過規則設定也可以看出來。

熔斷器的工作流程大致如下

Sentinel會為每個設定的規則都建立一個熔斷器,熔斷器有三種狀態,OPEN(開啟)、HALF_OPEN(半開)、CLOSED(關閉)

  • 當處於CLOSED狀態時,可以存取資源,存取之後會進行慢呼叫比例、異常比例、異常數的統計,一旦達到了設定的閾值,就會將熔斷器的狀態設定為OPEN

  • 當處於OPEN狀態時,會去判斷是否達到了熔斷時間,如果沒到,拒絕存取,如果到了,那麼就將狀態改成HALF_OPEN,然後存取資源,存取之後會對存取結果進行判斷,符合規則設定的要求,直接將熔斷器設定為CLOSED,關閉熔斷器,不符合則還是改為OPEN狀態

  • 當處於HALF_OPEN狀態時,直接拒絕存取資源

一般來說,熔斷降級其實是對於服務的呼叫方來說的。

在專案中會經常呼叫其它服務或者是第三方介面,而對於這些介面,一旦它們出現不穩定,就有可能導致自身服務長時間等待,從而出現響應延遲等等問題。

此時服務呼叫方就可基於熔斷降級方式解決。

一旦第三方介面響應時間過長,那麼就可以使用慢呼叫比例規則,當出現大量長時間響應的情況,那麼就直接熔斷,不去請求。

雖然說熔斷降級是針對服務的呼叫方來說,但是Sentinel本身並沒有限制熔斷降級一定是呼叫其它的服務。

總結

通過整篇文章的分析之後,再回頭看看Sentinel的簡介的內容,其實就能更好地理解Sentinel的定位和功能。

Sentinel核心就是一堆統計資料和基於這些統計資料實現的流控和熔斷的功能,原始碼並不複雜,而且Sentinel的程式碼寫得非常好。

最後Sentinel原始碼註釋倉庫地址:

https://github.com/sanyou3/sentinel.git

本文demo程式碼倉庫地址:

https://github.com/sanyou3/sentinel-demo

往期熱門文章推薦

扒一扒Nacos、OpenFeign、Ribbon、loadbalancer元件協調工作的原理

如何去閱讀原始碼,我總結了18條心法

如何實現延遲任務,我總結了11種方法

如何寫出漂亮程式碼,我總結了45個小技巧

三萬字盤點Spring/Boot的那些常用擴充套件點

兩萬字盤點那些被玩爛了的設計模式

掃碼或者搜尋關注公眾號 三友的java日記 ,及時乾貨不錯過,公眾號致力於通過畫圖加上通俗易懂的語言講解技術,讓技術更加容易學習,回覆 面試 即可獲得一套面試真題。