限流是保障服務(wù)高可用的方式之一,尤其是在微服務(wù)架構(gòu)中,對接口或資源進(jìn)行限流可以有效地保障服務(wù)的可用性和穩(wěn)定性。
之前的項目中使用的限流措施主要是Guava的RateLimiter。RateLimiter是基于令牌桶流控算法,使用非常簡單,但是功能相對比較少。
而現(xiàn)在,我們有了一種新的選擇,阿里提供的Sentinel。
Sentinel 是阿里巴巴提供的一種限流、熔斷中間件,與RateLimiter相比,Sentinel提供了豐富的限流、熔斷功能。它支持控制臺配置限流、熔斷規(guī)則,支持集群限流,并可以將相應(yīng)服務(wù)調(diào)用情況可視化。
目前已經(jīng)有很多項目接入了Sentinel,而本文主要是對Sentinel的限流功能做一次詳細(xì)的分析,至于Sentinel的其他能力,則不作深究。
一:總體流程:
從設(shè)計模式上來看,典型的的責(zé)任鏈模式。外部請求進(jìn)來后,要經(jīng)過責(zé)任鏈上各個節(jié)點(diǎn)的處理,而Sentinel的限流、熔斷就是通過責(zé)任鏈上的這些節(jié)點(diǎn)實(shí)現(xiàn)的。
從限流算法來看,Sentinel使用滑動窗口算法來進(jìn)行限流。要想深入了解原理,還是得從源碼上入手,下面,直接進(jìn)入Sentinel的源碼閱讀。
二:源碼解讀:
1,總體流程:
讀源碼先得找到源碼入口。我們經(jīng)常使用@ SentinelResource來標(biāo)記一個方法,可以將這個被@ SentinelResource標(biāo)記的方法看成是一個Sentinel資源。因此,我們以@ SentinelResource為入口,找到其切面,看看切面攔截后所做的工作,就可以明確Sentinel的工作原理了。直接看注解@SentinelResource的切面代碼(SentinelResourceAspect)。
可以清晰的看到Sentinel的行為方式。進(jìn)入SentinelResource切面后,會執(zhí)行SphU.entry方法,在這個方法中會對被攔截方法做限流和熔斷的邏輯處理。
如果觸發(fā)熔斷和限流,會拋出BlockException,我們可以指定blockHandler方法來處理BlockException。而對于業(yè)務(wù)上的異常,我們也可以配置fallback方法來處理被攔截方法調(diào)用產(chǎn)生的異常。
所以,Sentinel熔斷限流的處理主要是在SphU.entry方法中,其主要處理邏輯見下圖源碼。
可見,在SphU.entry方法中,Sentinel實(shí)現(xiàn)限流、熔斷等功能的流程可以總結(jié)如下:
獲取Sentinel上下文(Context);
獲取資源對應(yīng)的責(zé)任鏈;
生成資源調(diào)用憑證(Entry);
執(zhí)行責(zé)任鏈中各個節(jié)點(diǎn)。
接下來,圍繞這幾個方面,對Sentinel的服務(wù)機(jī)制做一個系統(tǒng)的闡述。
2,獲取Sentinel上下文(Context)
Context,顧名思義,就是Sentinel熔斷限流執(zhí)行的上下文,包含資源調(diào)用的節(jié)點(diǎn)和Entry信息。
來看看Context的特征:
Context是線程持有的,利用ThreadLocal與當(dāng)前線程綁定
Context包含的內(nèi)容
這里就引出了Sentinel的三個比較重要的概念:Conetxt,Node,Entry。這三個類是Sentinel的核心類,提供了資源調(diào)用路徑、資源調(diào)用統(tǒng)計等信息。
Context
Context是當(dāng)前線程所持有的Sentinel上下文。
進(jìn)入Sentinel的邏輯時,會首先獲取當(dāng)前線程的Context,如果沒有則新建。當(dāng)任務(wù)執(zhí)行完畢后,會清除當(dāng)前線程的context。Context 代表調(diào)用鏈路上下文,貫穿一次調(diào)用鏈路中的所有 Entry。
Context 維持著入口節(jié)點(diǎn)(entranceNode)、本次調(diào)用鏈路的 當(dāng)前節(jié)點(diǎn)(curNode)、調(diào)用來源(origin)等信息。Context 名稱即為調(diào)用鏈路入口名稱。
Node
Node是對一個@SentinelResource標(biāo)記的資源的統(tǒng)計包裝。
Context中記錄本當(dāng)前線程資源調(diào)用的入口節(jié)點(diǎn)。
我們可以通過入口節(jié)點(diǎn)的childList,可以追溯資源的調(diào)用情況。而每個節(jié)點(diǎn)都對應(yīng)一個@SentinelResource標(biāo)記的資源及其統(tǒng)計數(shù)據(jù),例如:passQps,blockQps,rt等數(shù)據(jù)。
Entry
Entry是Sentinel中用來表示是否通過限流的一個憑證,如果能正常返回,則說明你可以訪問被Sentinel保護(hù)的后方服務(wù),否則Sentinel會拋出一個BlockException。
另外,它保存了本次執(zhí)行entry()方法的一些基本信息,包括資源的Context、Node、對應(yīng)的責(zé)任鏈等信息,后續(xù)完成資源調(diào)用后,還需要更具獲得的這個Entry去執(zhí)行一些善后操作,包括退出Entry對應(yīng)的責(zé)任鏈,完成節(jié)點(diǎn)的一些統(tǒng)計信息更新,清除當(dāng)前線程的Context信息等。
3.? 獲取@SentinelResource標(biāo)記資源對應(yīng)的責(zé)任鏈
資源對應(yīng)的責(zé)任鏈?zhǔn)窍蘖鬟壿嬀唧w執(zhí)行的地方,采用的是典型的責(zé)任鏈模式。
默認(rèn)的責(zé)任鏈中的處理節(jié)點(diǎn)包括NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot、FlowSlot、DegradeSlot等。調(diào)用鏈(ProcessorSlotChain)和其中包含的所有Slot都實(shí)現(xiàn)了ProcessorSlot接口,采用責(zé)任鏈的模式執(zhí)行各個節(jié)點(diǎn)的處理邏輯,并調(diào)用下一個節(jié)點(diǎn)。
此外,相同資源(@SentinelResource標(biāo)記的方法)對應(yīng)的責(zé)任鏈?zhǔn)且恢碌摹R簿褪钦f,每個資源對應(yīng)一條單獨(dú)的責(zé)任鏈,可以看下源碼中資源責(zé)任鏈的獲取邏輯:先從緩存獲取,沒有則新建。
4. 生成調(diào)用憑證Entry
生成的Entry是CtEntry。其構(gòu)造參數(shù)包括資源包裝(ResourceWrapper)、資源對應(yīng)的責(zé)任鏈以及當(dāng)前線程的Context。
可以看到,新建CtEntry記錄了當(dāng)前資源的責(zé)任鏈和Context,同時更新Context,將Context的當(dāng)前Entry設(shè)置為自己。可以看到,CtEntry是一個雙向鏈表,構(gòu)建了Sentinel資源的調(diào)用鏈路。
5,責(zé)任鏈的執(zhí)行
接下來就進(jìn)入了責(zé)任鏈的執(zhí)行。責(zé)任鏈和其中的Slot都實(shí)現(xiàn)了ProcessorSlot,責(zé)任鏈的entry方法會依次執(zhí)行責(zé)任鏈各個slot,所以下面就進(jìn)入了責(zé)任鏈中的各個Slot。為了突出重點(diǎn),這次本文只研究與限流功能有關(guān)的Slot。
5.1??????NodeSelectorSlot -- 獲取當(dāng)前資源對應(yīng)Node,構(gòu)建節(jié)點(diǎn)調(diào)用樹
此節(jié)點(diǎn)負(fù)責(zé)獲取或者構(gòu)建當(dāng)前資源對應(yīng)的Node,這個Node被用于后續(xù)資源調(diào)用的統(tǒng)計及限流和熔斷條件的判斷。同時,NodeSelectorSlot還會完成調(diào)用鏈路構(gòu)建。來看源碼:
熟悉的代碼風(fēng)格。我們知道一個資源對應(yīng)一個責(zé)任鏈。每個調(diào)用鏈中都有NodeSelectorSlot。NodeSelectSlot中的node緩存map是非靜態(tài)變量,所以map只對當(dāng)前這個資源共用,不同的資源對應(yīng)的NodeSelectSlot及Node的緩存都是不一樣的,資源和Node緩存map的關(guān)系可見下圖。
所以NodeSelectorSlot的的作用是:
在資源對應(yīng)的調(diào)用鏈執(zhí)行時,獲取當(dāng)前context對應(yīng)的Node,這個Node代表著這個資源的調(diào)用情況。
將獲取到的node設(shè)為當(dāng)前node,添加到之前的node后面,形成樹狀的調(diào)用路徑。(通過Context中的當(dāng)前Entry進(jìn)行)
觸發(fā)下一個Slot的執(zhí)行。
這里有個很有趣的問題,就是我們在責(zé)任鏈的NodeSelectorSlot中獲取資源對應(yīng)的Node時,為什么用的是Context的name,而不是SentinelResource的name呢?
首先,我們知道一個資源對應(yīng)一條責(zé)任鏈。但是進(jìn)入一個資源調(diào)用的Context卻可能是不同的。如果使用資源名來作為key,獲取對應(yīng)的Node,那么通過不同context進(jìn)來的調(diào)用方法獲取到的Node就都是同一個了。所以通過這種方式,可以將相同resource對應(yīng)的node按Context區(qū)分開。
舉個例子,Sentinel功能的實(shí)現(xiàn)不僅僅可以通過@SentinelResource注解方法來實(shí)現(xiàn),也可以通過引入相關(guān)依賴(sentinel-dubbo-adapter),利用Dubbo的Filter機(jī)制直接對DUBBO接口進(jìn)行保護(hù)。我們來比較@SentinelResource和Dubbo方式生成Context的區(qū)別:
Dubbo Filter方式
生成的context的name是Dubbo的接口限定名或者方法限定名。
如果出現(xiàn)嵌套在Dubbo Filter方式下面的其他SentinelResource的資源調(diào)用,那么這些資源調(diào)用的就會就會出現(xiàn)不同的Context。
所以有這樣一種情況,不同的dubbo接口進(jìn)來,這些dubbo接口都調(diào)用了同一個@SentinelResource標(biāo)記的方法,那么這個方法對應(yīng)的SentinelReource的在執(zhí)行時對應(yīng)的Context就是不同的。
另一個問題是,既然資源按Context分出了不同的node,那我們想看資源總數(shù)統(tǒng)計是怎么辦呢?這就涉及到ClusterNode了。詳細(xì)可見ClusterBuilderSlot。
5.2? ?ClusterBuilderSlot -- 聚合相同資源不同Context的Node
此節(jié)點(diǎn)負(fù)責(zé)聚合相同資源不同Context對應(yīng)的Node,以供后續(xù)限流判斷使用。
可以看到,ClusterNode的獲取是以資源名為key。ClusterNode將會成為當(dāng)前node的一個屬性,主要目的是為了聚合同一個資源不同Context情況下的多個node。默認(rèn)的限流條件判斷就是依據(jù)ClusterNode中的統(tǒng)計信息來進(jìn)行的
5.3 StatisticSlot -- 資源調(diào)用統(tǒng)計
此節(jié)點(diǎn)主要負(fù)責(zé)資源調(diào)用的統(tǒng)計信息的計算和更新。與前面以及后面的slot不同,StatisticSlot的執(zhí)行時先觸發(fā)下一個slot的執(zhí)行,等下面的slot執(zhí)行完才會執(zhí)行自己的邏輯。
這也很好理解,作為統(tǒng)計組件,總要等熔斷或者限流處理完之后才能做統(tǒng)計吧。下面看一下具體的統(tǒng)計過程。
上面這張圖已經(jīng)很清晰的描述了StatisticSlot的數(shù)據(jù)統(tǒng)計的過程。可以注意一下無異常和阻塞異常的情況,主要是更新線程數(shù)、通過請求數(shù)量和阻塞請求數(shù)量。不管是DefaultNode,還是ClusterNode,都繼承自StatisticNode。所以Node的數(shù)據(jù)更新要來到StatisticNode。
參考Sentinel數(shù)據(jù)統(tǒng)計框圖,描述了Node統(tǒng)計數(shù)據(jù)更新的大體流程如下:
我們從StatisticNode.addPassRequest()方法入手,以passQps為例,探究StatisticNode是如何更新通過請求的QPS計數(shù)的。
從源碼可見,計數(shù)變量rollingCounterInSecond和rollingCounterInMinute都是Metric,兩個變量的時間維度分別是秒和分鐘。rollingCounterInSecond和rollingCounterInMinute用的是Metric的實(shí)現(xiàn)類ArrayMetric。
從ArrayMetric追溯下去:
統(tǒng)計信息都是保存到ArrayMetric的data,也就是LeapArray<MertricBucket>中的。
LeapArray是時間窗口數(shù)組。基本信息包括:時間窗口長度(ms,windowLengthInMs),取樣數(shù)(也就是時間窗口的數(shù)量,sampleCount),時間間隔(ms,intervalInMs),以及時間窗口數(shù)組(array)。時間窗口長度、取樣數(shù)及時間間隔有下面的關(guān)系:
windowLengthInMs = intervalInMs / sampleCount
代碼中rollingCounterInSecond使用的intervalInMs 是1000(ms),也就是1s,sampleCount=2。所以,窗口時長就是windowLengthInMs = 500ms。rollingCounterInMinute使用的intervalInMs 是60 * 1000(ms),也就是60s。sampleCount=60,所以,windowLengthInMs = 1000ms,也就是1s。
時間窗口數(shù)組(array)是類型是AtomicReferenceArray,可見這是一個原子操作的的數(shù)組引用。數(shù)組元素類型是WindowWrap<MetricBucket>。windowWrap是對時間窗口的一個包裝,包括窗口的開始時間(windowStart)及窗口的長度(windowLengthInMs),以及本窗口的計數(shù)器(value,類型為MetricBucket)。窗口實(shí)際的計數(shù)是由MetricBucket進(jìn)行的,計數(shù)信息是保存在MetricBucket里計數(shù)器counters(類型為(LongAdder))。可以看一下下圖計數(shù)組件的組成框圖:
回到StatisticNode.addPassRequest方法,以rollingCounterInSecond.addPass(count)為例,探究Sentinel如何進(jìn)行滑動窗口計數(shù)的。
5.3.1?獲取當(dāng)前時間窗口
(1)取當(dāng)前時間戳對應(yīng)的數(shù)組下標(biāo)
long timeId = time / windowLength
int idx = (int)(timeId % array.length());
time為當(dāng)前時間,windowLength為時間窗口長度,rollingCounterInSecond的時間窗口長度是500ms。array 是單位時間內(nèi)時間窗口的數(shù)量,rollingCounterInSecond的單位時間(1s)時間窗口數(shù)是2。timeId是當(dāng)前時間對時間窗口的整除。time每增加一個windowLength的長度,timeId就會增加1,時間窗口就會往前滑動一個。
(2)計算窗口開始時間
窗口開始時間 = 當(dāng)前時間(ms)-當(dāng)前時間(ms)%時間窗口長度(ms)
獲取的窗口開始時間均為時間窗口的整數(shù)倍。
(3)獲取時間窗口
首先,根據(jù)數(shù)組下標(biāo)從LeapArray的數(shù)組中獲取時間窗口。
如果獲取到的時間窗口自為空,則新建時間窗口(CAS)。
如果獲取到的時間窗口非空,且時間窗口的開始時間等于我們計算的開始時間,說明當(dāng)前時間正好在這個時間窗口里,直接返回該時間窗口。
?如果獲取到的時間窗口非空,且時間窗口的開始時間小于我們計算的開始時間,說明時間窗口已經(jīng)過期(距離上次獲取時間窗口已經(jīng)過去比較久的場景),需要更新時間窗口(加鎖操作),將時間窗口的開始時間設(shè)為計算出來的開始時間,將時間窗口里的計數(shù)器重置為0。
?如果獲取到的時間窗口非空,且時間窗口的開始時間大于我們計算的開始時間,創(chuàng)建新的時間窗口。這個一般不會走進(jìn)這個分支,因為說明當(dāng)前時間已經(jīng)落后于時間窗口了,獲取到的時間窗口是將來的時間,那就沒有意義了。
5.3.2?對時間窗口的計數(shù)器進(jìn)行累加
時間窗口計數(shù)器是一個LongAdder數(shù)組,這個數(shù)組用于存放通過請求數(shù)、異常請求數(shù)、阻塞請求數(shù)等數(shù)據(jù)。如下圖:
其中,通過計數(shù)、阻塞計數(shù)、異常計數(shù)為執(zhí)行StatisticSlot的entry方法時更新。成功計數(shù)及響應(yīng)時間是執(zhí)行StatisticSlot的exit方法時更新。其實(shí)就是分別在被攔截方法執(zhí)行前和執(zhí)行后進(jìn)行相應(yīng)計數(shù)的更新。當(dāng)然,addPass就是在計數(shù)數(shù)組的第一個元素上進(jìn)行累加。
計數(shù)數(shù)組元素類型是LongAdder。LongAdder是JDK8添加到JUC中的。它是一個線程安全的、比Atomic*系工具性能更好的"計數(shù)器"。
5.4 FlowSlot -- 限流判斷
FlowSlot是進(jìn)行限流條件判斷的節(jié)點(diǎn)。之前在StatisticSlot對相關(guān)資源調(diào)用做的統(tǒng)計,在FlowSlot限流判斷時將會得到使用。
直接來到限流操作的核心邏輯–限流規(guī)則檢查器(FlowRuleChecker):
主要的流程包括:
獲取資源對應(yīng)的限流規(guī)則
根據(jù)限流規(guī)則檢查是否被限流
如果被限流,則拋出限流異常FlowException。FlowException繼承自BlockException。
那么FlowSlot檢查是否限流的過程是怎么樣的?
默認(rèn)情況下,限流使用的節(jié)點(diǎn)是當(dāng)前節(jié)點(diǎn)的cluster node。主要分析的限流方式是QPS限流。來看一下限流的關(guān)鍵代碼(DefaultController):
獲取節(jié)點(diǎn)的當(dāng)前qps計數(shù);
判斷獲取新的計數(shù)后是否超過閾值
超過閾值單返回false,表示被限流,后面會拋出FlowException。否則返回true,不被限流。
可以看到限流判斷非常簡單,只需要對qps計數(shù)進(jìn)行檢查就可以了。這歸功于StatisticSlot做的數(shù)據(jù)統(tǒng)計。
NodeSelectorSlot用于獲取資源對應(yīng)的Node,并構(gòu)建Node調(diào)用樹,將SentinelSource的調(diào)用鏈路以Node Tree的形式組起來。ClusterBuilderSlot為當(dāng)前Node創(chuàng)建對應(yīng)的ClusterNode,聚合相同資源對應(yīng)的不同Context的Node,后續(xù)的限流依據(jù)就是這個ClusterNode。
ClusterNode繼承自StatisticNode,記錄著相應(yīng)資源處理的一些統(tǒng)計數(shù)據(jù)。StatisticSlot用于更新資源調(diào)用的相關(guān)計數(shù),用于后續(xù)的限流判斷使用。FlowSlot根據(jù)資源對應(yīng)Node的調(diào)用計數(shù),判斷是否進(jìn)行限流。至此,Sentinel的責(zé)任鏈執(zhí)行邏輯就完整了。
6,Sentienl 的收尾工作
無論執(zhí)行成功還是失敗,或者是阻塞,都會執(zhí)行Entry.exit()方法,來看一下這個方法。
判斷要退出的entry是否是當(dāng)前context的當(dāng)前entry;
如果要退出的entry不是當(dāng)前context的當(dāng)前entry,則不退出此entry,而是退出context的的當(dāng)前entry及其所有父entry,并拋出異常;
如果要退出的entry是當(dāng)前context的當(dāng)前entry(這種是正常情況),先退出當(dāng)前entry對應(yīng)的責(zé)任鏈的所有slot。在這一步,StatisticSlot會更新node的success計數(shù)和RT計數(shù);
將context的當(dāng)前entry置為被退出的entry的父entry;
如果被退出entry的父entry為空,且context為默認(rèn)context,自動退出默認(rèn)context(清除ThreadLocal)。
清除被退出entry的context引用
總結(jié)
通過閱讀Sentinel的源碼,可以很清晰的理解Sentinel的限流過程了,而對上面的源碼閱讀,總結(jié)如下:
三大組件Context、Entry、Node,是Sentinel的核心組件,各類信息及資源調(diào)用情況都由這三大類持有;
采用責(zé)任鏈模式完成Sentinel的信息統(tǒng)計、熔斷、限流等操作;
責(zé)任鏈中NodeSelectSlot負(fù)責(zé)選擇當(dāng)前資源對應(yīng)的Node,同時構(gòu)建node調(diào)用樹;
責(zé)任鏈中ClusterBuilderSlot負(fù)責(zé)構(gòu)建當(dāng)前Node對應(yīng)的ClusterNode,用于聚合同一資源對應(yīng)不同Context的Node;
責(zé)任鏈中的StatisticSlot用于統(tǒng)計當(dāng)前資源的調(diào)用情況,更新Node與其對用的ClusterNode的各種統(tǒng)計數(shù)據(jù);
責(zé)任鏈中的FlowSlot根據(jù)當(dāng)前Node對應(yīng)的ClusterNode(默認(rèn))的統(tǒng)計信息進(jìn)行限流;
資源調(diào)用統(tǒng)計數(shù)據(jù)(例如PassQps)使用滑動時間窗口進(jìn)行統(tǒng)計;
所有工作執(zhí)行完畢后,執(zhí)行退出流程,補(bǔ)充一些統(tǒng)計數(shù)據(jù),清理Context。