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);
    }

}

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

所以上面這段代碼的整體意思就是對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

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

而真正的原因是 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 個:

這些實現會通過 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:

資源調用者很好理解,比如一個 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(關閉)

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

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

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

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

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

總結

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

Sentinel 核心就是一堆統計數據和基於這些統計數據實現的流控和熔斷的功能,源碼並不複雜,而且 Sentinel 的代碼寫得非常好。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ig-3kDCxSWErrzy2SY3A5Q