本文介紹阿里開(kāi)源的 Sentinel 源碼,GitHub: alibaba/Sentinel,基于當(dāng)前(2019-10-21)最新的 release 版本 1.7.0。
總體來(lái)說(shuō),Sentinel 的源碼比較簡(jiǎn)單,復(fù)雜的部分在于它的模型對(duì)于初學(xué)者來(lái)說(shuō)不好理解。
雖然本文不是很長(zhǎng),最后兩節(jié)還和主流程無(wú)關(guān),但是,本文對(duì)于源碼分析已經(jīng)非常細(xì)致了。
閱讀建議:在閱讀本文前,你應(yīng)該至少了解過(guò) Sentinel 是什么,如果使用過(guò) Sentinel 或已經(jīng)閱讀過(guò)部分源碼那就更好了。
另外,本文不涉及到集群流控。由于很多讀者也沒(méi)使用過(guò) Hystrix,所以本文也不做任何對(duì)比。
更新 2019-12-11:更新了滑動(dòng)窗口秒級(jí)數(shù)據(jù)統(tǒng)計(jì) OccupiableBucketLeapArray 的分析。
簡(jiǎn)介
Sentinel 的定位是流量控制、熔斷降級(jí),你應(yīng)該把它理解為一個(gè)第三方 Jar 包。
這個(gè) Jar 包會(huì)進(jìn)行流量統(tǒng)計(jì),執(zhí)行流量控制規(guī)則。而統(tǒng)計(jì)數(shù)據(jù)的展示和規(guī)則的設(shè)置在 sentinel-dashboard 項(xiàng)目中,這是一個(gè) Spring MVC 應(yīng)用,有后臺(tái)管理界面,我們通過(guò)這個(gè)管理后臺(tái)和各個(gè)應(yīng)用進(jìn)行交互。
當(dāng)然,你不一定需要 dashboard,很長(zhǎng)一段時(shí)間,我僅僅使用 sentinel-core,它會(huì)將統(tǒng)計(jì)信息寫入到指定的文件中,我通過(guò)該文件內(nèi)容來(lái)了解每個(gè)接口的流量情況。當(dāng)然,這種情況下,我只是使用到了 Sentinel 的流量監(jiān)控功能而已。
從左側(cè)我們可以看到這個(gè) dashboard 可以管理很多應(yīng)用,而對(duì)于每個(gè)應(yīng)用,我們還可以有很多機(jī)器實(shí)例(見(jiàn)機(jī)器列表)。我們?cè)谶@個(gè)后臺(tái),可以非常直觀地了解到每個(gè)接口的 QPS 數(shù)據(jù),我們可以對(duì)每個(gè)接口設(shè)置流量控制規(guī)則、降級(jí)規(guī)則等。
這個(gè) dashboard 默認(rèn)是不持久化數(shù)據(jù)的,它的所有數(shù)據(jù)都是在內(nèi)存中的,所以 dashboard 重啟意味著所有的數(shù)據(jù)都會(huì)丟失。你應(yīng)該按照自己的需要來(lái)使用 dashboard,如至少你應(yīng)該要持久化規(guī)則設(shè)置,QPS 數(shù)據(jù)非常適合存放在時(shí)序數(shù)據(jù)庫(kù)中,當(dāng)然如果你的數(shù)據(jù)量不大,存 MySQL 也問(wèn)題不大,定期清理一下過(guò)期數(shù)據(jù)即可,因?yàn)榇蟛糠秩藨?yīng)該不會(huì)關(guān)心一個(gè)月以前的 QPS 數(shù)據(jù)。
sentinel-dashboard 并沒(méi)有定位為一個(gè)功能強(qiáng)大的管理后臺(tái),一般來(lái)說(shuō),我們需要基于它來(lái)進(jìn)行二次開(kāi)發(fā),甚至于你也可以不使用這個(gè) Java 項(xiàng)目,自己使用其他的語(yǔ)言來(lái)實(shí)現(xiàn)。在最后一小節(jié),我介紹了業(yè)務(wù)應(yīng)用是怎么和 dashboard 應(yīng)用交互的。
Sentinel 的數(shù)據(jù)統(tǒng)計(jì)
在正式開(kāi)始介紹 Sentinel 的流程源碼之前,我想先和大家介紹一下 Sentinel 的數(shù)據(jù)統(tǒng)計(jì)模塊的內(nèi)容,這樣讀者在后面看到相應(yīng)的內(nèi)容的時(shí)候心里有一些底。這節(jié)內(nèi)容還是比較簡(jiǎn)單的,當(dāng)然,如果你希望立馬進(jìn)入 Sentinel 的主流程,可以先跳過(guò)這一節(jié)。
Sentinel 的定位是流量控制,它有兩個(gè)維度的控制,一個(gè)是控制并發(fā)線程數(shù),另一個(gè)是控制 QPS,它們都是針對(duì)某個(gè)具體的接口來(lái)設(shè)置的,其實(shí)說(shuō)資源比較準(zhǔn)確,Sentinel 把控制的粒度定義為 Resource。
既然要做控制,那么首先,Sentinel 就要先做統(tǒng)計(jì),它要知道當(dāng)前接口的 QPS 和并發(fā)是多少,進(jìn)而判斷一個(gè)新的請(qǐng)求能不能讓它通過(guò)。
數(shù)據(jù)統(tǒng)計(jì)的代碼在 StatisticNode 中,對(duì)于 QPS 數(shù)據(jù),它使用了滑動(dòng)窗口的設(shè)計(jì):
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
private AtomicInteger curThreadNum = new AtomicInteger(0);
AtomicInteger 用于統(tǒng)計(jì)并發(fā)量(curThreadNum)非常簡(jiǎn)單,就是原子加、原子減的操作,這里不浪費(fèi)篇幅了,下面僅介紹 QPS 的統(tǒng)計(jì)。
從上面的代碼可以知道,Sentinel 統(tǒng)計(jì)了 秒 和 分 兩個(gè)維度的數(shù)據(jù),下面我們簡(jiǎn)單說(shuō)說(shuō)實(shí)現(xiàn)類 ArrayMetric 的源碼設(shè)計(jì)。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public class ArrayMetric implements Metric {
private final LeapArray<MetricBucket> data;
public ArrayMetric(int sampleCount, int intervalInMs) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
if (enableOccupy) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}
......
}
ArrayMetric 的內(nèi)部是一個(gè) LeapArray,我們以分鐘維度統(tǒng)計(jì)的使用來(lái)說(shuō),它使用子類 BucketLeapArray 實(shí)現(xiàn)。
這里先介紹較為簡(jiǎn)單的 BucketLeapArray 的實(shí)現(xiàn),然后在最后一節(jié)會(huì)介紹 OccupiableBucketLeapArray。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public abstract class LeapArray<T> {
protected int windowLengthInMs;
protected int sampleCount;
protected int intervalInMs;
protected final AtomicReferenceArray<WindowWrap<T>> array;
// 對(duì)于分鐘維度的設(shè)置,sampleCount 為 60,intervalInMs 為 60 * 1000
public LeapArray(int sampleCount, int intervalInMs) {
// 單個(gè)窗口長(zhǎng)度,這里是 1000ms
this.windowLengthInMs = intervalInMs / sampleCount;
// 一輪總時(shí)長(zhǎng) 60,000 ms
this.intervalInMs = intervalInMs;
// 60 個(gè)窗口
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<>(sampleCount);
}
// ......
}
它的內(nèi)部核心是一個(gè)數(shù)組 array,它的長(zhǎng)度為 60,也就是有 60 個(gè)窗口,每個(gè)窗口長(zhǎng)度為 1 秒,剛好一分鐘走完一輪。然后下一輪開(kāi)啟“覆蓋”操作。
每個(gè)窗口是一個(gè) WindowWrap 類實(shí)例。
- 添加數(shù)據(jù)的時(shí)候,先判斷當(dāng)前走到哪個(gè)窗口了(當(dāng)前時(shí)間(s) % 60 即可),然后需要判斷這個(gè)窗口是否是過(guò)期數(shù)據(jù),如果是過(guò)期數(shù)據(jù)(窗口代表的時(shí)間距離當(dāng)前已經(jīng)超過(guò) 1 分鐘),需要先重置這個(gè)窗口實(shí)例的數(shù)據(jù)。
- 統(tǒng)計(jì)數(shù)據(jù)同理,如統(tǒng)計(jì)過(guò)去一分鐘的 QPS 數(shù)據(jù),就是將每個(gè)窗口的值相加,當(dāng)中需要判斷窗口數(shù)據(jù)是否是過(guò)期數(shù)據(jù),即判斷窗口的 WindowWrap 實(shí)例是否是一分鐘內(nèi)的數(shù)據(jù)。
核心邏輯都封裝在了 currentWindow(long timeMillis) 和 values(long timeMillis)方法中。
添加數(shù)據(jù)的時(shí)候,我們要先獲取操作的目標(biāo)窗口,也就是 currentWindow 這個(gè)方法,Sentinel 在這里處理初始化和過(guò)期重置的情況:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 獲取窗口下標(biāo)
int idx = calculateTimeIdx(timeMillis);
// 計(jì)算該窗口的理論開(kāi)始時(shí)間
long windowStart = calculateWindowStart(timeMillis);
// 嵌套在一個(gè)循環(huán)中,因?yàn)橛胁l(fā)的情況
while (true) {
WindowWrap<T> old = array.get(idx);
if (old == null) {
// 窗口未實(shí)例化的情況,使用一個(gè) CAS 來(lái)設(shè)置該窗口實(shí)例
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
// 存在競(jìng)爭(zhēng)
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
// 當(dāng)前數(shù)組中的窗口沒(méi)有過(guò)期
return old;
} else if (windowStart > old.windowStart()) {
// 該窗口已過(guò)期,重置窗口的值。使用一個(gè)鎖來(lái)控制并發(fā)。
if (updateLock.tryLock()) {
try {
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
// 正常情況都不會(huì)走到這個(gè)分支,異常情況其實(shí)就是時(shí)鐘回?fù)埽@里返回一個(gè) WindowWrap 是容錯(cuò)
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
獲取數(shù)據(jù),使用的是 values 方法,這個(gè)方法返回“有效的”窗口中的數(shù)據(jù):
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public List<T> values(long timeMillis) {
if (timeMillis < 0) {
return new ArrayList<T>();
}
int size = array.length();
List<T> result = new ArrayList<T>(size);
for (int i = 0; i < size; i++) {
WindowWrap<T> windowWrap = array.get(i);
// 過(guò)濾掉過(guò)期數(shù)據(jù)
if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
continue;
}
result.add(windowWrap.value());
}
return result;
}
// 判斷當(dāng)前窗口的數(shù)據(jù)是否是 60 秒內(nèi)的
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
return time - windowWrap.windowStart() > intervalInMs;
}
這個(gè) values 方法很簡(jiǎn)單,就是過(guò)濾掉那些過(guò)期數(shù)據(jù)就可以了。
到這里,我們就說(shuō)完了 分 維度數(shù)據(jù)統(tǒng)計(jì)的問(wèn)題。至于秒維度的數(shù)據(jù)統(tǒng)計(jì),有些不一樣,稍微復(fù)雜一些,我在后面單獨(dú)起了一節(jié)。跳過(guò)這部分內(nèi)容對(duì)閱讀 Sentinel 源碼沒(méi)有影響。
Sentinel 源碼分析
下面,我們正式開(kāi)始 Sentinel 的源碼介紹。
官方文檔中,它的最簡(jiǎn)單的使用是下面這樣的,這里用了 try-with-resource 的寫法:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
try (Entry entry = SphU.entry("HelloWorld")) {
// Your business logic here.
System.out.println("hello world");
} catch (BlockException e) {
// Handle rejected request.
e.printStackTrace();
}
這個(gè)例子對(duì)于理解源碼其實(shí)不是很好,我們來(lái)寫一個(gè)復(fù)雜一些的例子,這樣對(duì)理解源碼有很大的幫助:
1、紅色部分,Context 代表一個(gè)調(diào)用鏈的入口,Context 實(shí)例設(shè)置在 ThreadLocal 中,所以它是跟著線程走的,如果要切換線程,需要手動(dòng)切換。ContextUtil#enter 有兩個(gè)參數(shù):
第一個(gè)參數(shù)是 context name,它代表調(diào)用鏈的入口,作用是為了區(qū)分不同的調(diào)用鏈路,個(gè)人感覺(jué)沒(méi)什么用,默認(rèn)是 Constants.CONTEXT_DEFAULT_NAME 的常量值 "sentinel_default_context";
第二個(gè)參數(shù)代表調(diào)用方標(biāo)識(shí) origin,目前它有兩個(gè)作用,一是用于黑白名單的授權(quán)控制,二是可以用來(lái)統(tǒng)計(jì)諸如從應(yīng)用 application-a 發(fā)起的對(duì)當(dāng)前應(yīng)用 interfaceXxx() 接口的調(diào)用,目前這個(gè)數(shù)據(jù)會(huì)被統(tǒng)計(jì),但是 dashboard 中并不展示。
2、進(jìn)入 BlockException 異常分支,代表該次請(qǐng)求被流量控制規(guī)則限制了,我們一般會(huì)讓代碼走入到熔斷降級(jí)的邏輯里面。當(dāng)然,BlockException 其實(shí)有好多個(gè)子類,如 DegradeException、FlowException 等,我們也可以 catch 具體的子類來(lái)進(jìn)行處理。
3、Entry 是我們的重點(diǎn),對(duì)于 SphU#entry 方法:
第一個(gè)參數(shù)標(biāo)識(shí)資源,通常就是我們的接口標(biāo)識(shí),對(duì)于數(shù)據(jù)統(tǒng)計(jì)、規(guī)則控制等,我們一般都是在這個(gè)粒度上進(jìn)行的,根據(jù)這個(gè)字符串來(lái)唯一標(biāo)識(shí),它會(huì)被包裝成 ResourceWrapper 實(shí)例,大家要先看下它的 hashCode 和 equals 方法;
第二個(gè)參數(shù)標(biāo)識(shí)資源的類型,我們左邊的代碼使用了 EntryType.IN 代表這個(gè)是入口流量,比如我們的接口對(duì)外提供服務(wù),那么我們通常就是控制入口流量;EntryType.OUT 代表出口流量,比如上面的 getOrderInfo 方法(沒(méi)寫默認(rèn)就是 OUT),它的業(yè)務(wù)需要調(diào)用訂單服務(wù),像這種情況,壓力其實(shí)都在訂單服務(wù)中,那么我們就指定它為出口流量。這個(gè)流量類型有什么用呢?答案在 SystemSlot 類中,它用于實(shí)現(xiàn)自適應(yīng)限流,根據(jù)系統(tǒng)健康狀態(tài)來(lái)判斷是否要限流,如果是 OUT 類型,由于壓力在外部系統(tǒng)中,所以就不需要執(zhí)行這個(gè)規(guī)則。
4、上面的代碼,我們?cè)?getOrderInfo 中嵌套使用了 Entry,也是為了我們后面的源碼分析需要。如果我們?cè)谝粋€(gè)方法中寫的話,要注意內(nèi)層的 Entry 先 exit,才能做外層的 exit,否則會(huì)拋出異常。源碼角度來(lái)看,是在 Context 實(shí)例中,保存了當(dāng)前的 Entry 實(shí)例。
5、實(shí)際開(kāi)發(fā)過(guò)程中,我們當(dāng)然不會(huì)每個(gè)接口都像上面的代碼這么寫,Sentinel 提供了很多的擴(kuò)展和適配器,這里只是為了源碼分析的需要。
Sentinel 提供了很多的 adapter 用于諸如 dubbo、grpc、網(wǎng)關(guān)等環(huán)境,它們其實(shí)都是封裝了上述的代碼。你只要認(rèn)真看完本文,那些包裝都很容易看懂。
這里我們介紹了 Sentinel 的接口使用,不過(guò)它的類名字我現(xiàn)在都沒(méi)懂是什么意思,SphU、CtSph、CtEntry 這些名字有什么特殊含義,有知道的讀者請(qǐng)不吝賜教。
下面,我們按照上面的代碼,開(kāi)始源碼分析。這里我不會(huì)像之前分析 Spring IOC 和 Netty 源碼一樣,一行一行代碼說(shuō),所以大家一定要打開(kāi)源碼配合著看。
ContextUtil#enter
我們先看 Context#enter 方法,這行代碼我們是可以不寫的,下面我們就會(huì)看到,如果我們不顯式調(diào)用這個(gè)方法,那么會(huì)進(jìn)入到默認(rèn)的 context 中。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
ContextUtil.enter("user-center", "app-A");
進(jìn)入到 ContextUtil 類,大家可能會(huì)漏看它的 static 代碼塊,這里會(huì)添加一個(gè)默認(rèn)的 EntranceNode 實(shí)例。
然后上面的這個(gè)方法會(huì)走到 ContextUtil#trueEnter 中,這里會(huì)添加名為 "user-center" 的 EntranceNode 節(jié)點(diǎn)。根據(jù)源碼,我們可以得出下面這棵樹(shù):
這里的源碼非常簡(jiǎn)單,如果我們從來(lái)不顯式調(diào)用 ContextUtil#enter 方法的話,那 root 就只有一個(gè) default 子節(jié)點(diǎn)。
context 很好理解,它代表線程執(zhí)行的上下文,在各種開(kāi)源框架中都有類似的語(yǔ)義,在 Sentinel 中,我們可以看到,對(duì)于一個(gè)新的 context name,Sentinel 會(huì)往樹(shù)中添加一個(gè) EntranceNode 實(shí)例。它的作用是為了區(qū)分調(diào)用鏈路,標(biāo)識(shí)調(diào)用入口。在 sentinel-board 中,我們可以很直觀地看出調(diào)用鏈路:
SphU#entry
接下來(lái),我們看 SphU#entry。自己跟進(jìn)去,我們會(huì)來(lái)到 CtSph#entryWithPriority 方法,這個(gè)方法是 Sentinel 的骨架,非常重要。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
throws BlockException {
// 從 ThreadLocal 中獲取 Context 實(shí)例
Context context = ContextUtil.getContext();
// 如果是 NullContext,那么說(shuō)明 context name 超過(guò)了 2000 個(gè),參見(jiàn) ContextUtil#trueEnter
// 這個(gè)時(shí)候,Sentinel 不再接受處理新的 context 配置,也就是不做這些新的接口的統(tǒng)計(jì)、限流熔斷等
if (context instanceof NullContext) {
return new CtEntry(resourceWrapper, null, context);
}
// 我們前面說(shuō)了,如果我們不顯式調(diào)用 ContextUtil#enter,這里會(huì)進(jìn)入到默認(rèn)的 context 中
if (context == null) {
context = MyContextUtil.myEnter(Constants.CONTEXT_DEFAULT_NAME, "", resourceWrapper.getType());
}
// Sentinel 的全局開(kāi)關(guān),Sentinel 提供了接口讓用戶可以在 dashboard 開(kāi)啟/關(guān)閉
if (!Constants.ON) {
return new CtEntry(resourceWrapper, null, context);
}
// 設(shè)計(jì)模式中的責(zé)任鏈模式。
// 下面這行代碼用于構(gòu)建一個(gè)責(zé)任鏈,入?yún)⑹?resource,前面我們說(shuō)過(guò)資源的唯一標(biāo)識(shí)是 resource name
ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
// 根據(jù) lookProcessChain 方法,我們知道,當(dāng) resource 超過(guò) Constants.MAX_SLOT_CHAIN_SIZE,
// 也就是 6000 的時(shí)候,Sentinel 開(kāi)始不處理新的請(qǐng)求,這么做主要是為了 Sentinel 的性能考慮
if (chain == null) {
return new CtEntry(resourceWrapper, null, context);
}
// 執(zhí)行這個(gè)責(zé)任鏈。如果拋出 BlockException,說(shuō)明鏈上的某一環(huán)拒絕了該請(qǐng)求,
// 把這個(gè)異常往上層業(yè)務(wù)層拋,業(yè)務(wù)層處理 BlockException 應(yīng)該進(jìn)入到熔斷降級(jí)邏輯中
Entry e = new CtEntry(resourceWrapper, chain, context);
try {
chain.entry(context, resourceWrapper, null, count, prioritized, args);
} catch (BlockException e1) {
e.exit(count, args);
throw e1;
} catch (Throwable e1) {
// This should not happen, unless there are errors existing in Sentinel internal.
RecordLog.info("Sentinel unexpected exception", e1);
}
return e;
}
這里說(shuō)一說(shuō) lookProcessChain(resourceWrapper) 這個(gè)方法。Sentinel 的處理核心都在這個(gè)責(zé)任鏈中,鏈中每一個(gè)節(jié)點(diǎn)是一個(gè) Slot 實(shí)例,這個(gè)鏈通過(guò)異常來(lái)告知調(diào)用入口最終的執(zhí)行情況。
大家自己點(diǎn)進(jìn)去源碼,這個(gè)責(zé)任鏈由 SlotChainProvider#newSlotChain 生產(chǎn),Sentinel 提供了 SPI 端點(diǎn),讓我們可以自己定制 Builder,如添加一個(gè) Slot 進(jìn)去。由于 SlotChainBuilder 接口設(shè)計(jì)的問(wèn)題,我們只能全局所有的 resource 使用相同的責(zé)任鏈配置。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public class DefaultSlotChainBuilder 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 AuthoritySlot());
chain.addLast(new SystemSlot());
chain.addLast(new FlowSlot());
chain.addLast(new DegradeSlot());
return chain;
}
}
接下來(lái),我們就按照默認(rèn)的 DefaultSlotChainBuilder 生成的責(zé)任鏈往下看源碼。
這里要強(qiáng)調(diào)一點(diǎn),對(duì)于相同的 resource,使用同一個(gè)責(zé)任鏈實(shí)例,不同的 resource,使用不同的責(zé)任鏈實(shí)例。
NodeSelectorSlot
首先,鏈中第一個(gè)處理節(jié)點(diǎn)是 NodeSelectorSlot。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
// key 是 context name, value 是 DefaultNode 實(shí)例
private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
throws Throwable {
DefaultNode node = map.get(context.getName());
if (node == null) {
synchronized (this) {
node = map.get(context.getName());
if (node == null) {
node = new DefaultNode(resourceWrapper, null);
HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
cacheMap.putAll(map);
cacheMap.put(context.getName(), node);
map = cacheMap;
// Build invocation tree
((DefaultNode) context.getLastNode()).addChild(node);
}
}
}
context.setCurNode(node);
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
</pre>
我們前面說(shuō)了,責(zé)任鏈實(shí)例和 resource name 相關(guān),和線程無(wú)關(guān),所以當(dāng)處理同一個(gè) resource 的時(shí)候,會(huì)進(jìn)入到同一個(gè) NodeSelectorSlot 實(shí)例中。
所以這塊代碼主要就是要處理:不同的 context name,同一個(gè) resource name 的情況。如:
上面的代碼示例了同一個(gè)資源 getUserInfo,在兩個(gè) context name 中進(jìn)入。然后我們?cè)俳Y(jié)合前面的那棵樹(shù),我們可以得出下面這棵樹(shù):
NodeSelectorSlot 還是比較簡(jiǎn)單的,只要讀者搞清楚 NodeSelectorSlot 實(shí)例是跟著 resource 一一對(duì)應(yīng)的就很清楚了。
ClusterBuilderSlot
接下來(lái),我們來(lái)到了 ClusterBuilderSlot 這一環(huán),這一環(huán)的主要作用是構(gòu)建 ClusterNode。
這里不貼源碼,根據(jù)上面的樹(shù),然后在經(jīng)過(guò)該類的處理以后,我們可以得出下面這棵樹(shù):
從上圖可以看到,對(duì)于每一個(gè) resource,這里會(huì)對(duì)應(yīng)一個(gè) ClusterNode 實(shí)例,如果不存在,就創(chuàng)建一個(gè)實(shí)例。
這個(gè) ClusterNode 非常有用,因?yàn)槲覀兙褪鞘褂盟鼇?lái)做數(shù)據(jù)統(tǒng)計(jì)的。比如 getUserInfo 這個(gè)接口,由于從不同的 context name 中開(kāi)啟調(diào)用鏈,它有多個(gè) DefaultNode 實(shí)例,但是只有一個(gè) ClusterNode,通過(guò)這個(gè)實(shí)例,我們可以知道這個(gè)接口現(xiàn)在的 QPS 是多少。
另外,這個(gè)類還處理了 origin 不是默認(rèn)值的情況:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
if (!"".equals(context.getOrigin())) {
Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
context.getCurEntry().setOriginNode(originNode);
}
我們可以看到,當(dāng)設(shè)置了 origin 的時(shí)候,會(huì)額外生成一個(gè) StatisticsNode 實(shí)例,掛在 ClusterNode 上。
我們把前面的代碼改改,看紅色部分:
我們的 getUserInfo 接收到了來(lái)自 application-a 和 application-b 兩個(gè)應(yīng)用的請(qǐng)求,那么樹(shù)會(huì)變成下面這樣:
它的作用是用來(lái)統(tǒng)計(jì)從 application-a 過(guò)來(lái)的訪問(wèn) getUserInfo 這個(gè)接口的信息。目前這個(gè)信息在 dashboard 中是不展示的,畢竟也沒(méi)什么用。
LogSlot
這個(gè)類比較簡(jiǎn)單,我們看到它直接 fire 出去了,也就是說(shuō),先處理責(zé)任鏈上后面的那些節(jié)點(diǎn),如果它們拋出了 BlockException,那么這里才做處理。
這里調(diào)用了 EagleEyeLogUtil#log 方法,它其實(shí)就是,將被設(shè)置的規(guī)則 block 的信息記錄到日志文件 sentinel-block.log 中。
StatisticSlot
這個(gè) slot 非常重要,它負(fù)責(zé)進(jìn)行數(shù)據(jù)統(tǒng)計(jì)。
它也是先 fire 出去,等后面的節(jié)點(diǎn)處理完畢以后,它再進(jìn)行統(tǒng)計(jì)數(shù)據(jù)。之所以這么設(shè)計(jì),是因?yàn)楹竺娴墓?jié)點(diǎn)是做控制的,執(zhí)行的時(shí)候可能是正常通過(guò)的,也可能是拋出 BlockException 異常的。
源碼非常簡(jiǎn)單,對(duì)于 QPS 統(tǒng)計(jì),使用前面介紹的滑動(dòng)窗口,而對(duì)于線程并發(fā)的統(tǒng)計(jì),它使用了 LongAdder。
大家一定要看一遍這個(gè)類的源碼,這里沒(méi)有什么特別的內(nèi)容需要強(qiáng)調(diào),所以我就不展開(kāi)說(shuō)了。
接下來(lái),我們后面要介紹的幾個(gè) Slot,需要通過(guò) dashboard 進(jìn)行開(kāi)啟,因?yàn)樾枰渲靡?guī)則。
AuthoritySlot
這個(gè)類非常簡(jiǎn)單,根據(jù) origin 做黑白名單的控制:
在 dashboard 中,是這么配置的:
這里的調(diào)用方就是我們前面介紹的 origin。
SystemSlot
規(guī)則校驗(yàn)都在 SystemRuleManager#checkSystem 中:
我們先說(shuō)說(shuō)上面的代碼中的 RT、線程數(shù)、入口 QPS 這三項(xiàng)系統(tǒng)保護(hù)規(guī)則。dashboard 配置界面:
在前面介紹的 StatisticSlot 類中,有下面一段代碼:
Sentinel 針對(duì)所有的入口流量,使用了一個(gè)全局的 ENTRY_NODE 進(jìn)行統(tǒng)計(jì),所以我們也要知道,系統(tǒng)保護(hù)規(guī)則是全局的,和具體的某個(gè)資源沒(méi)有關(guān)系。
由于系統(tǒng)的平均 RT、當(dāng)前線程數(shù)、QPS 都可以從 ENTRY_NODE 中獲得,所以限制代碼非常簡(jiǎn)單,比較一下大小就可以了。如果超過(guò)閾值,拋出 SystemBlockException。
ENTRY_NODE 是 ClusterNode 類型的,而 ClusterNode 對(duì)于 rt、qps 都是統(tǒng)計(jì)的秒維度的數(shù)據(jù)。
當(dāng)然,對(duì)于 SystemSlot 類來(lái)說(shuō),最重要的其實(shí)并不是上面的這些,因?yàn)樵趯?shí)際使用過(guò)程中,對(duì)于 RT、線程數(shù)、QPS 每一項(xiàng),我們其實(shí)都很難設(shè)置一個(gè)確定的閾值。
我們往下看它的對(duì)于系統(tǒng)負(fù)載和 CPU 資源的保護(hù):
我們可以看到,Sentinel 通過(guò)調(diào)用 MBean 中的方法獲取當(dāng)前的系統(tǒng)負(fù)載和 CPU 使用率,Sentinel 起了一個(gè)后臺(tái)線程,每秒查詢一次。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
currentLoad = osBean.getSystemLoadAverage();
currentCpuUsage = osBean.getSystemCpuLoad();
</pre>
下圖展示 dashboard 中對(duì)于 CPU 使用率的規(guī)則配置:
FlowSlot
Flow Control 是 Sentinel 的核心, 因?yàn)?Sentinel 本身定位就是一個(gè)流控工具,所以 FlowSlot 非常重要。
對(duì)于讀者來(lái)說(shuō),最大的挑戰(zhàn)應(yīng)該也是這部分代碼,因?yàn)榍懊娴拇a,只要讀者理得清楚里面各個(gè)類的關(guān)系,就不難。而這部分代碼由于涉及到限流算法,會(huì)稍微復(fù)雜一點(diǎn)點(diǎn)。
DegradeSlot
恭喜大家,終于到最后一個(gè) slot 了。
它有三個(gè)策略,我們首先說(shuō)說(shuō)根據(jù) RT 降級(jí):
如果按照上面的配置:對(duì)于 getUserInfo 這個(gè)資源,正常情況下,它只需要 50ms 就夠了,如果它的 RT 超過(guò)了 100ms,那么它會(huì)進(jìn)入半降級(jí)狀態(tài),接下來(lái)的 5 次訪問(wèn),如果都超過(guò)了 100ms,那么在接下來(lái)的 10 秒內(nèi),所有的請(qǐng)求都會(huì)被拒絕。
其實(shí)這個(gè)描述不是百分百準(zhǔn)確,打開(kāi) DegradeRule#passCheck 源碼,我們用代碼來(lái)描述:
Sentinel 使用了 cut 作為開(kāi)關(guān),開(kāi)啟這個(gè)開(kāi)關(guān)以后,會(huì)啟動(dòng)一個(gè)定時(shí)任務(wù),過(guò)了 10秒 以后關(guān)閉這個(gè)開(kāi)關(guān)。
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
if (cut.compareAndSet(false, true)) {
ResetTask resetTask = new ResetTask(this);
pool.schedule(resetTask, timeWindow, TimeUnit.SECONDS);
}
對(duì)于異常比例和異常數(shù)的控制,非常簡(jiǎn)單,大家看一下源碼就懂了。同理,達(dá)到閾值,開(kāi)啟斷路器,之后由定時(shí)任務(wù)關(guān)閉,這里就不浪費(fèi)篇幅了。
應(yīng)用和 sentinel-dashboard 的交互
這里花點(diǎn)篇幅介紹一下客戶端是怎么和 dashboard 進(jìn)行交互的。
在 Sentinel 的源碼中,打開(kāi) sentinel-transport 工程,可以看到三個(gè)子工程,common 是基礎(chǔ)包和接口定義。
如果客戶端要接入 dashboard,可以使用 netty-http 或 simple-http 中的一個(gè)。為什么不直接使用 Netty,而要同時(shí)提供 http 的選項(xiàng)呢?那是因?yàn)槟悴灰欢ㄊ褂?Java 來(lái)實(shí)現(xiàn) dashboard,如果我們使用其他語(yǔ)言來(lái)實(shí)現(xiàn) dashboard 的話,使用 http 協(xié)議比較容易適配。
下面我們只介紹 http 的使用,首先,添加 simple-http 依賴:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.6.3</version>
</dependency>
然后在應(yīng)用啟動(dòng)參數(shù)中添加 dashboard 服務(wù)器地址,同時(shí)可以指定當(dāng)前應(yīng)用的名稱:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
-Dcsp.sentinel.dashboard.server=127.0.0.1:8080 -Dproject.name=sentinel-learning
這個(gè)時(shí)候我們打開(kāi) dashboard 是看不到這個(gè)應(yīng)用的,因?yàn)闆](méi)有注冊(cè)。
當(dāng)我們?cè)诘谝淮问褂?Sentinel 以后,Sentinel 會(huì)自動(dòng)注冊(cè)。
下面帶大家看看過(guò)程是怎樣的。首先,我們?cè)谑褂?Sentinel 的時(shí)候會(huì)調(diào)用 SphU#entry:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public static Entry entry(String name) throws BlockException {
return Env.sph.entry(name, EntryType.OUT, 1, OBJECTS0);
}
這里使用了 Env 類,其實(shí)就是這個(gè)類做的事情:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public class Env {
public static final Sph sph = new CtSph();
static {
// If init fails, the process will exit.
InitExecutor.doInit();
}
}
進(jìn)到 InitExecutor.doInit 方法:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public static void doInit() {
if (!initialized.compareAndSet(false, true)) {
return;
}
try {
ServiceLoader<InitFunc> loader = ServiceLoader.load(InitFunc.class);
List<OrderWrapper> initList = new ArrayList<OrderWrapper>();
for (InitFunc initFunc : loader) {
insertSorted(initList, initFunc);
}
for (OrderWrapper w : initList) {
w.func.init();
}
// ...
}
這里使用 SPI 加載 InitFunc 的實(shí)現(xiàn),大家可以在這里斷個(gè)點(diǎn),可以發(fā)現(xiàn)這里加載了 CommandCenterInitFunc 類和 HeartbeatSenderInitFunc 類。
前者是客戶端啟動(dòng)的接口服務(wù),提供給 dashboard 查詢數(shù)據(jù)和規(guī)則設(shè)置使用的。后者用于客戶端主動(dòng)發(fā)送心跳信息給 dashboard。
我們看 HeartbeatSenderInitFunc#init 方法:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
@Override
public void init() {
HeartbeatSender sender = HeartbeatSenderProvider.getHeartbeatSender();
if (sender == null) {
RecordLog.warn("[HeartbeatSenderInitFunc] WARN: No HeartbeatSender loaded");
return;
}
initSchedulerIfNeeded();
long interval = retrieveInterval(sender);
setIntervalIfNotExists(interval);
// 啟動(dòng)一個(gè)定時(shí)器,發(fā)送心跳信息
scheduleHeartbeatTask(sender, interval);
}
這里看到,init 方法的第一行就是去加載 HeartbeatSender 的實(shí)現(xiàn)類,這里又用到了 SPI 的機(jī)制,如果我們添加了 sentinel-transport-simple-http 這個(gè)依賴,那么 SimpleHttpHeartbeatSender 就會(huì)被加載。
之后在上面的最后一行代碼,啟動(dòng)了一個(gè)定時(shí)器,以一定的間隔(默認(rèn)10秒)不斷地發(fā)送心跳信息到 dashboard 應(yīng)用,這個(gè)心跳信息中就包含應(yīng)用的名稱、ip、port、Sentinel 版本 等信息。
而對(duì)于 dashboard 來(lái)說(shuō),有了這些信息,就可以對(duì)應(yīng)用進(jìn)行規(guī)則設(shè)置、到應(yīng)用拉取數(shù)據(jù)用于頁(yè)面展示等。
Sentinel 在客戶端并沒(méi)有使用第三方 http 包,而是自己基于 JDK 的 Socket 和 ServerSocket 接口實(shí)現(xiàn)了簡(jiǎn)單的客戶端和服務(wù)端,主要也是為了不增加依賴。
Sentinel 中秒級(jí) QPS 的統(tǒng)計(jì)問(wèn)題
以下內(nèi)容建立在你對(duì)于滑動(dòng)窗口有了較為深入的了解的基礎(chǔ)上,如果你覺(jué)得有點(diǎn)吃力,說(shuō)明你對(duì)于 Sentinel 還不是完全熟悉,可以選擇性放棄這一節(jié)的內(nèi)容。
我們前面介紹了滑動(dòng)窗口用在 分 維度的數(shù)據(jù)統(tǒng)計(jì)上,當(dāng)我們?cè)谡f(shuō) QPS 的時(shí)候,當(dāng)然我們一般指的是秒維度的數(shù)據(jù)。當(dāng)然,你在很多地方看到的 QPS 數(shù)據(jù),其實(shí)都是通過(guò)分維度的數(shù)據(jù)來(lái)得到的,包括 metrics 日志文件、dashboard 中的 QPS。
下面,我們深入分析秒維度數(shù)據(jù)統(tǒng)計(jì)的一些問(wèn)題。
在開(kāi)始的時(shí)候,我們說(shuō)了 Sentinel 統(tǒng)計(jì)了 分 和 秒 兩個(gè)維度的數(shù)據(jù):
1、對(duì)于 分 來(lái)說(shuō),一輪是 60 秒,分為 60 個(gè)時(shí)間窗口,每個(gè)時(shí)間窗口是 1 秒;
2、對(duì)于 秒 來(lái)說(shuō),一輪是 1 秒,分為 2 個(gè)時(shí)間窗口,每個(gè)時(shí)間窗口是 0.5 秒;
如果我們用上面介紹的統(tǒng)計(jì)分維度的 BucketLeapArray 來(lái)統(tǒng)計(jì)秒維度數(shù)據(jù)可以嗎?答案當(dāng)然是不行,因?yàn)闀?huì)不準(zhǔn)確。
設(shè)想一個(gè)場(chǎng)景,我們的一個(gè)資源,訪問(wèn)的 QPS 穩(wěn)定是 10,假設(shè)請(qǐng)求是均勻分布的,在相對(duì)時(shí)間 0.0 - 1.0 秒?yún)^(qū)間,通過(guò)了 10 個(gè)請(qǐng)求,我們?cè)?1.1 秒的時(shí)候,觀察到的 QPS 可能只有 5,因?yàn)榇藭r(shí)第一個(gè)時(shí)間窗口被重置了,只有第二個(gè)時(shí)間窗口有值。
這個(gè)大家應(yīng)該很容易理解,如果你覺(jué)得不理解,可以不用浪費(fèi)時(shí)間在這節(jié)了
所以,我們可以知道,如果用 BucketLeapArray 來(lái)實(shí)現(xiàn),會(huì)有 0~50% 的數(shù)據(jù)誤差,這肯定是不能接受的。
那能不能增加窗口的數(shù)量來(lái)降低誤差到一個(gè)合理的范圍內(nèi)呢?這個(gè)大家可以思考一下,考慮一下它對(duì)于性能是否有較大的損失。
大家翻開(kāi) StatisticNode 的源碼,對(duì)于秒維度數(shù)據(jù)統(tǒng)計(jì),Sentinel 使用下面的構(gòu)造方法:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
// 2 個(gè)時(shí)間窗口,每個(gè)窗口長(zhǎng)度 0.5 秒
public ArrayMetric(int sampleCount, int intervalInMs) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
OccupiableBucketLeapArray 實(shí)現(xiàn)類的源碼并不長(zhǎng),我們大概看一眼,可以發(fā)現(xiàn)它的 newEmptyBucket 和 resetWindowTo 這兩個(gè)方法和 BucketLeapArray 有點(diǎn)不一樣,也就是在重置的時(shí)候,它不是直接重置成 0 的。
所以,我們要大膽猜測(cè)一下,這個(gè)類里面的 borrowArray 做了一些事情,它是 FutureBucketLeapArray 的實(shí)例,這個(gè)類和前面接觸的 BucketLeapArray 差不多,但是加了一個(gè) Future 單詞。這里我們先仔細(xì)看看它。
它和 BucketLeapArray 唯一的不同是,它覆寫了下面這個(gè)方法:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
@Override
public boolean isWindowDeprecated(long time, WindowWrap<MetricBucket> windowWrap) {
// Tricky: will only calculate for future.
return time >= windowWrap.windowStart();
}
</pre>
我們發(fā)現(xiàn),如果按照它的這種定義,在調(diào)用 values() 方法的時(shí)候,所有的 2 個(gè)窗口都是過(guò)期的,將得不到任何的值。所以,我們大概可以判斷,給這個(gè)數(shù)組添加值的時(shí)候,使用的時(shí)間應(yīng)該不是當(dāng)前時(shí)間,而是一個(gè)未來(lái)的時(shí)間點(diǎn)。這大概就是 Future 要表達(dá)的意思。
我們?cè)倩氐?OccupiableBucketLeapArray 這個(gè)類,可以看到在重置的時(shí)候,它使用了 borrowArray 的值:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
@Override
protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
// Update the start time and reset value.
w.resetTo(time);
MetricBucket borrowBucket = borrowArray.getWindowValue(time);
if (borrowBucket != null) {
w.value().reset();
w.value().addPass((int)borrowBucket.pass());
} else {
w.value().reset();
}
return w;
}
所以我們大概可以猜一猜它是怎么利用這個(gè) FutureBucketLeapArray 實(shí)例的:borrowArray 存儲(chǔ)了未來(lái)的時(shí)間窗口的值。當(dāng)主線到達(dá)某個(gè)時(shí)間窗口的時(shí)候,如果發(fā)現(xiàn)當(dāng)前時(shí)間窗口是過(guò)期的,前面介紹過(guò),會(huì)需要重置這個(gè)窗口,這個(gè)時(shí)候,它會(huì)檢查一下 borrowArray 是否有值,如果有,將其作為這個(gè)窗口的初始值填充進(jìn)來(lái),而不是簡(jiǎn)單重置為 0 值。
有了這個(gè)思路,我們?cè)倏?borrowArray 中的值是怎么進(jìn)來(lái)的。
我們很容易可以找到,只可能通過(guò)這里的 addWaiting 方法設(shè)置:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
@Override
public void addWaiting(long time, int acquireCount) {
WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
window.value().add(MetricEvent.PASS, acquireCount);
}
接下來(lái),我們找這個(gè)方法被哪里調(diào)用了,找到最后,我們發(fā)現(xiàn)只有 DefaultController 這個(gè)類中有調(diào)用。
這個(gè)類是流控中的 “快速失敗” 規(guī)則控制器,我們簡(jiǎn)單看一下代碼:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
int curCount = avgUsedTokens(node);
if (curCount + acquireCount > count) {
// 只有設(shè)置了 prioritized 的情況才會(huì)進(jìn)入到下面的 if 分支
// 也就是說(shuō),對(duì)于一般的場(chǎng)景,被限流了,就快速失敗
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
// 下面的這行 tryOccupyNext 非常復(fù)雜,大意就是說(shuō)去占有"未來(lái)的"令牌
// 可以看到,下面做了 sleep,為了保證 QPS 不會(huì)因?yàn)轭A(yù)占而撐大
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
// 就是這里設(shè)置了 borrowArray 的值
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
// PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
看到這里,我其實(shí)還有很多疑問(wèn)沒(méi)有被解開(kāi)
首先,這里解開(kāi)了一個(gè)問(wèn)題,就是這個(gè)類為什么叫 OccupiableBucketLeapArray?
- Occupiable 這里代表可以被預(yù)占的意思,結(jié)合上面 DefaultController 的源碼,可以知道它原來(lái)是用來(lái)滿足 prioritized 類型的資源的,我們可以認(rèn)為這類請(qǐng)求有較高的優(yōu)先級(jí)。如果 QPS 達(dá)到閾值,這類資源通常不能用快速失敗返回, 而是讓它去預(yù)占未來(lái)的 QPS 容量。
當(dāng)然,令人失望的是,這里根本沒(méi)有解開(kāi) QPS 是怎么準(zhǔn)確計(jì)算的這個(gè)問(wèn)題。
下面,我思路倒回來(lái),我來(lái)證明 Sentinel 的秒維度的 QPS 統(tǒng)計(jì)是不準(zhǔn)確的:
<pre style="margin: 0px; padding: 0px; border: 0px; font: inherit; vertical-align: baseline; word-break: break-word;">
public static void main(String[] args) {
// 下面幾行代碼設(shè)置了 QPS 閾值是 100
FlowRule rule = new FlowRule("test");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(100);
rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
List<FlowRule> list = new ArrayList<>();
list.add(rule);
FlowRuleManager.loadRules(list);
// 先通過(guò)一個(gè)請(qǐng)求,讓 clusterNode 先建立起來(lái)
try (Entry entry = SphU.entry("test")) {
} catch (BlockException e) {
}
// 起一個(gè)線程一直打印 qps 數(shù)據(jù)
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
System.out.println(ClusterBuilderSlot.getClusterNode("test").passQps());
}
}
}).start();
while (true) {
try (Entry entry = SphU.entry("test")) {
Thread.sleep(5);
} catch (BlockException e) {
// ignore
} catch (InterruptedException e) {
// ignore
}
}
}
大家跑一下代碼,然后觀察下輸出,QPS 數(shù)據(jù)在 50~100 這個(gè)區(qū)間一直變化,印證了我前面說(shuō)的,秒級(jí) QPS 統(tǒng)計(jì)是極度不準(zhǔn)確的。
根據(jù)前面的分析,其實(shí)也沒(méi)有什么結(jié)論要說(shuō)了。剩下的交給大家自己去思考,去探索,這個(gè)過(guò)程一定比看我的文章更有意思。
原文地址:https://javadoop.com/post/sentinel
書籍推薦 Redis 深度歷險(xiǎn):核心原理和應(yīng)用實(shí)踐
獲取方式:關(guān)注然后簡(jiǎn)信“資料”即可獲得文檔領(lǐng)取方式