Sentinel 是如何做限流的


限流是保障服務高可用的方式之一,尤其是在微服務架構中,對接口或資源進行限流可以有效地保障服務的可用性和穩定性。

之前的項目中使用的限流措施主要是Guava的RateLimiter。RateLimiter是基於令牌桶流控算法,使用非常簡單,但是功能相對比較少。

而現在,我們有了一種新的選擇,阿里提供的 Sentinel。

Sentinel 是阿里巴巴提供的一種限流、熔斷中間件,與RateLimiter相比,Sentinel提供了豐富的限流、熔斷功能。它支持控制台配置限流、熔斷規則,支持集群限流,並可以將相應服務調用情況可視化。

目前已經有很多項目接入了Sentinel,而本文主要是對Sentinel的限流功能做一次詳細的分析,至於Sentinel的其他能力,則不作深究。

一、總體流程

先來了解一下總體流程:

( 引用於Sentinel官網)

上面的圖是官網的圖,

從設計模式上來看,典型的的責任鏈模式。外部請求進來后,要經過責任鏈上各個節點的處理,而Sentinel的限流、熔斷就是通過責任鏈上的這些節點實現的。

從限流算法來看,Sentinel使用滑動窗口算法來進行限流。要想深入了解原理,還是得從源碼上入手,下面,直接進入Sentinel的源碼閱讀。

二、源碼閱讀

1. 源碼閱讀入口及總體流程

讀源碼先得找到源碼入口。我們經常使用@ SentinelResource來標記一個方法,可以將這個被@ SentinelResource標記的方法看成是一個Sentinel資源。因此,我們以@ SentinelResource為入口,找到其切面,看看切面攔截后所做的工作,就可以明確Sentinel的工作原理了。直接看注解@SentinelResource的切面代碼(SentinelResourceAspect)。

可以清晰的看到Sentinel的行為方式。進入SentinelResource切面后,會執行SphU.entry方法,在這個方法中會對被攔截方法做限流和熔斷的邏輯處理。

如果觸發熔斷和限流,會拋出BlockException,我們可以指定blockHandler方法來處理BlockException。而對於業務上的異常,我們也可以配置fallback方法來處理被攔截方法調用產生的異常。

所以,Sentinel熔斷限流的處理主要是在SphU.entry方法中,其主要處理邏輯見下圖源碼。

可見,在SphU.entry方法中,Sentinel實現限流、熔斷等功能的流程可以總結如下:

  • 獲取Sentinel上下文(Context);
  • 獲取資源對應的責任鏈;
  • 生成資源調用憑證(Entry);
  • 執行責任鏈中各個節點。

接下來,圍繞這幾個方面,對Sentinel的服務機制做一個系統的闡述。

2. 獲取Sentinel上下文(Context)

Context,顧名思義,就是Sentinel熔斷限流執行的上下文,包含資源調用的節點和Entry信息。

來看看Context的特征:

  • Context是線程持有的,利用ThreadLocal與當前線程綁定。

  • Context包含的內容

這里就引出了Sentinel的三個比較重要的概念:Conetxt,Node,Entry。這三個類是Sentinel的核心類,提供了資源調用路徑、資源調用統計等信息。

Context

Context是當前線程所持有的Sentinel上下文。

進入Sentinel的邏輯時,會首先獲取當前線程的Context,如果沒有則新建。當任務執行完畢后,會清除當前線程的context。Context 代表調用鏈路上下文,貫穿一次調用鏈路中的所有 Entry。

Context 維持着入口節點(entranceNode)、本次調用鏈路的 當前節點(curNode)、調用來源(origin)等信息。Context 名稱即為調用鏈路入口名稱。

Node

Node是對一個@SentinelResource標記的資源的統計包裝。

Context中記錄本當前線程資源調用的入口節點。

我們可以通過入口節點的childList,可以追溯資源的調用情況。而每個節點都對應一個@SentinelResource標記的資源及其統計數據,例如:passQps,blockQps,rt等數據。

Entry

Entry是Sentinel中用來表示是否通過限流的一個憑證,如果能正常返回,則說明你可以訪問被Sentinel保護的后方服務,否則Sentinel會拋出一個BlockException。

另外,它保存了本次執行entry()方法的一些基本信息,包括資源的Context、Node、對應的責任鏈等信息,后續完成資源調用后,還需要更具獲得的這個Entry去執行一些善后操作,包括退出Entry對應的責任鏈,完成節點的一些統計信息更新,清除當前線程的Context信息等。

3.  獲取@SentinelResource標記資源對應的責任鏈

資源對應的責任鏈是限流邏輯具體執行的地方,采用的是典型的責任鏈模式。

先來看看默認的的責任鏈的組成:

 

默認的責任鏈中的處理節點包括NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot、FlowSlot、DegradeSlot等。調用鏈(ProcessorSlotChain)和其中包含的所有Slot都實現了ProcessorSlot接口,采用責任鏈的模式執行各個節點的處理邏輯,並調用下一個節點。

每個節點都有自己的作用,后面將會看到這些節點具體是干什么的。

此外,相同資源(@SentinelResource標記的方法)對應的責任鏈是一致的。也就是說,每個資源對應一條單獨的責任鏈,可以看下源碼中資源責任鏈的獲取邏輯:先從緩存獲取,沒有則新建。

4. 生成調用憑證Entry

生成的Entry是CtEntry。其構造參數包括資源包裝(ResourceWrapper)、資源對應的責任鏈以及當前線程的Context。

可以看到,新建CtEntry記錄了當前資源的責任鏈和Context,同時更新Context,將Context的當前Entry設置為自己。可以看到,CtEntry是一個雙向鏈表,構建了Sentinel資源的調用鏈路。

5. 責任鏈的執行

接下來就進入了責任鏈的執行。責任鏈和其中的Slot都實現了ProcessorSlot,責任鏈的entry方法會依次執行責任鏈各個slot,所以下面就進入了責任鏈中的各個Slot。為了突出重點,這次本文只研究與限流功能有關的Slot。

5.1 NodeSelectorSlot -- 獲取當前資源對應Node,構建節點調用樹

此節點負責獲取或者構建當前資源對應的Node,這個Node被用於后續資源調用的統計及限流和熔斷條件的判斷。同時,NodeSelectorSlot還會完成調用鏈路構建。來看源碼:

熟悉的代碼風格。我們知道一個資源對應一個責任鏈。每個調用鏈中都有NodeSelectorSlot。NodeSelectSlot中的node緩存map是非靜態變量,所以map只對當前這個資源共用,不同的資源對應的NodeSelectSlot及Node的緩存都是不一樣的,資源和Node緩存map的關系可見下圖。

所以NodeSelectorSlot的的作用是:

  • 在資源對應的調用鏈執行時,獲取當前context對應的Node,這個Node代表着這個資源的調用情況。
  • 將獲取到的node設為當前node,添加到之前的node后面,形成樹狀的調用路徑。(通過Context中的當前Entry進行)
  • 觸發下一個Slot的執行。

這里有個很有趣的問題,就是我們在責任鏈的NodeSelectorSlot中獲取資源對應的Node時,為什么用的是Context的name,而不是SentinelResource的name呢?

首先,我們知道一個資源對應一條責任鏈。但是進入一個資源調用的Context卻可能是不同的。如果使用資源名來作為key,獲取對應的Node,那么通過不同context進來的調用方法獲取到的Node就都是同一個了。所以通過這種方式,可以將相同resource對應的node按Context區分開。

舉個例子,Sentinel功能的實現不僅僅可以通過@SentinelResource注解方法來實現,也可以通過引入相關依賴(sentinel-dubbo-adapter),利用Dubbo的Filter機制直接對DUBBO接口進行保護。我們來比較@SentinelResource和Dubbo方式生成Context的區別:

@SentinelResource

生成的context的name是:sentinel_default_context。所有資源對應的Context都是這個值。

Dubbo Filter方式

生成的context的name是Dubbo的接口限定名或者方法限定名。

如果出現嵌套在Dubbo Filter方式下面的其他SentinelResource的資源調用,那么這些資源調用的就會就會出現不同的Context。

所以有這樣一種情況,不同的dubbo接口進來,這些dubbo接口都調用了同一個@SentinelResource標記的方法,那么這個方法對應的SentinelReource的在執行時對應的Context就是不同的。

另一個問題是,既然資源按Context分出了不同的node,那我們想看資源總數統計是怎么辦呢?這就涉及到ClusterNode了。詳細可見ClusterBuilderSlot。

5.2 ClusterBuilderSlot -- 聚合相同資源不同Context的Node

此節點負責聚合相同資源不同Context對應的Node,以供后續限流判斷使用。

可以看到,ClusterNode的獲取是以資源名為key。ClusterNode將會成為當前node的一個屬性,主要目的是為了聚合同一個資源不同Context情況下的多個node。默認的限流條件判斷就是依據ClusterNode中的統計信息來進行的。

5.3 StatisticSlot -- 資源調用統計

此節點主要負責資源調用的統計信息的計算和更新。與前面以及后面的slot不同,StatisticSlot的執行時先觸發下一個slot的執行,等下面的slot執行完才會執行自己的邏輯。

這也很好理解,作為統計組件,總要等熔斷或者限流處理完之后才能做統計吧。下面看一下具體的統計過程。

上面這張圖已經很清晰的描述了StatisticSlot的數據統計的過程。可以注意一下無異常和阻塞異常的情況,主要是更新線程數、通過請求數量和阻塞請求數量。不管是DefaultNode,還是ClusterNode,都繼承自StatisticNode。所以Node的數據更新要來到StatisticNode。

參考Sentinel數據統計框圖,描述了Node統計數據更新的大體流程如下:

我們從StatisticNode.addPassRequest()方法入手,以passQps為例,探究StatisticNode是如何更新通過請求的QPS計數的。

從源碼可見,計數變量rollingCounterInSecond和rollingCounterInMinute都是Metric,兩個變量的時間維度分別是秒和分鍾。rollingCounterInSecond和rollingCounterInMinute用的是Metric的實現類ArrayMetric。

從ArrayMetric追溯下去:

統計信息都是保存到ArrayMetric的data,也就是LeapArray<MertricBucket>中的。

LeapArray是時間窗口數組。基本信息包括:時間窗口長度(ms,windowLengthInMs),取樣數(也就是時間窗口的數量,sampleCount),時間間隔(ms,intervalInMs),以及時間窗口數組(array)。時間窗口長度、取樣數及時間間隔有下面的關系:

回到StatisticNode.addPassRequest方法,以rollingCounterInSecond.addPass(count)為例,探究Sentinel如何進行滑動窗口計數的。

5.3.1 獲取當前時間窗口

(1)取當前時間戳對應的數組下標

窗口開始時間 = 當前時間(ms)-當前時間(ms)%時間窗口長度(ms)

獲取的窗口開始時間均為時間窗口的整數倍。

(3)獲取時間窗口

首先,根據數組下標從LeapArray的數組中獲取時間窗口。

  • 如果獲取到的時間窗口自為空,則新建時間窗口(CAS)。
  • 如果獲取到的時間窗口非空,且時間窗口的開始時間等於我們計算的開始時間,說明當前時間正好在這個時間窗口里,直接返回該時間窗口。
  • 如果獲取到的時間窗口非空,且時間窗口的開始時間小於我們計算的開始時間,說明時間窗口已經過期(距離上次獲取時間窗口已經過去比較久的場景),需要更新時間窗口(加鎖操作),將時間窗口的開始時間設為計算出來的開始時間,將時間窗口里的計數器重置為0。
  •  如果獲取到的時間窗口非空,且時間窗口的開始時間大於我們計算的開始時間,創建新的時間窗口。這個一般不會走進這個分支,因為說明當前時間已經落后於時間窗口了,獲取到的時間窗口是將來的時間,那就沒有意義了。

5.3.2 對時間窗口的計數器進行累加

時間窗口計數器是一個LongAdder數組,這個數組用於存放通過請求數、異常請求數、阻塞請求數等數據。如下圖:

其中,通過計數、阻塞計數、異常計數為執行StatisticSlot的entry方法時更新。成功計數及響應時間是執行StatisticSlot的exit方法時更新。其實就是分別在被攔截方法執行前和執行后進行相應計數的更新。當然,addPass就是在計數數組的第一個元素上進行累加。

計數數組元素類型是LongAdder。LongAdder是JDK8添加到JUC中的。它是一個線程安全的、比Atomic*系工具性能更好的"計數器"。

5.4 FlowSlot -- 限流判斷

FlowSlot是進行限流條件判斷的節點。之前在StatisticSlot對相關資源調用做的統計,在FlowSlot限流判斷時將會得到使用。

直接來到限流操作的核心邏輯–限流規則檢查器(FlowRuleChecker):

主要的流程包括:

  • 獲取資源對應的限流規則
  • 根據限流規則檢查是否被限流

如果被限流,則拋出限流異常FlowException。FlowException繼承自BlockException。

那么FlowSlot檢查是否限流的過程是怎么樣的?

默認情況下,限流使用的節點是當前節點的cluster node。主要分析的限流方式是QPS限流。來看一下限流的關鍵代碼(DefaultController):

  • 獲取節點的當前qps計數;
  • 判斷獲取新的計數后是否超過閾值
  • 超過閾值單返回false,表示被限流,后面會拋出FlowException。否則返回true,不被限流。

可以看到限流判斷非常簡單,只需要對qps計數進行檢查就可以了。這歸功於StatisticSlot做的數據統計。

5.5 責任鏈小結

通過上面的講解,再來看下面這張圖,是不是很清晰了?

( 引用於Sentinel官網)

NodeSelectorSlot用於獲取資源對應的Node,並構建Node調用樹,將SentinelSource的調用鏈路以Node Tree的形式組起來。ClusterBuilderSlot為當前Node創建對應的ClusterNode,聚合相同資源對應的不同Context的Node,后續的限流依據就是這個ClusterNode。

ClusterNode繼承自StatisticNode,記錄着相應資源處理的一些統計數據。StatisticSlot用於更新資源調用的相關計數,用於后續的限流判斷使用。FlowSlot根據資源對應Node的調用計數,判斷是否進行限流。至此,Sentinel的責任鏈執行邏輯就完整了。

6. Sentienl 的收尾工作

無論執行成功還是失敗,或者是阻塞,都會執行Entry.exit()方法,來看一下這個方法。

  • 判斷要退出的entry是否是當前context的當前entry;
  • 如果要退出的entry不是當前context的當前entry,則不退出此entry,而是退出context的的當前entry及其所有父entry,並拋出異常;
  • 如果要退出的entry是當前context的當前entry(這種是正常情況),先退出當前entry對應的責任鏈的所有slot。在這一步,StatisticSlot會更新node的success計數和RT計數;
  • 將context的當前entry置為被退出的entry的父entry;
  • 如果被退出entry的父entry為空,且context為默認context,自動退出默認context(清除ThreadLocal)。
  • 清除被退出entry的context引用

7. 總結

通過閱讀Sentinel的源碼,可以很清晰的理解Sentinel的限流過程了,而對上面的源碼閱讀,總結如下:

  • 三大組件Context、Entry、Node,是Sentinel的核心組件,各類信息及資源調用情況都由這三大類持有;
  • 采用責任鏈模式完成Sentinel的信息統計、熔斷、限流等操作;
  • 責任鏈中NodeSelectSlot負責選擇當前資源對應的Node,同時構建node調用樹;
  • 責任鏈中ClusterBuilderSlot負責構建當前Node對應的ClusterNode,用於聚合同一資源對應不同Context的Node;
  • 責任鏈中的StatisticSlot用於統計當前資源的調用情況,更新Node與其對用的ClusterNode的各種統計數據;
  • 責任鏈中的FlowSlot根據當前Node對應的ClusterNode(默認)的統計信息進行限流;
  • 資源調用統計數據(例如PassQps)使用滑動時間窗口進行統計;
  • 所有工作執行完畢后,執行退出流程,補充一些統計數據,清理Context。

三、參考文獻

https://github.com/alibaba/Sentinel/wiki

作者:Sun Yi


免責聲明!

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



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