Sentinel原理解析

完整源碼流程圖

Sentinel限流、熔斷降級源碼剖析.png

架構圖

image.png

幾個重要概念

Resource

Sentinel 通過資源來保護具體的業務代碼或其他后方服務。 Sentinel 把復雜的邏輯給屏蔽掉了,用戶只需要為受保護的代碼或服務定義一個資源,然后定義規則就可以了,剩下的通通交給 Sentinel 來處理了。并且資源和規則是解耦的,規則可以在運行時動態修改。定義完資源后,就可以通過在程序中埋點來保護你自己的服務了,埋點的方式有兩種:

  • try-catch 方式(通過 SphU.entry(...)),當 catch 到BlockException時執行異常處理(或fallback)
  • if-else 方式(通過 SphO.entry(...)),當返回 false 時執行異常處理(或fallback)

以上這兩種方式都是通過硬編碼的形式定義資源然后進行資源埋點的,對業務代碼的侵入太大.
Sentinel 也可以通過注解來定義資源,具體的注解為:SentinelResource 。通過注解除了可以定義資源外,還可以指定 blockHandler 和 fallback 方法。

Sentinel 中具體表示資源的類是:ResourceWrapper ,他是一個抽象的包裝類,包裝了資源的 Name 和EntryType。他有兩個實現類,分別是:StringResourceWrapper 和 MethodResourceWrapper

顧名思義,StringResourceWrapper 是通過對一串字符串進行包裝,是一個通用的資源包裝類,MethodResourceWrapper 是對方法調用的包裝。

image.png

Slot

Sentinel 的工作流程就是圍繞著一個個插槽所組成的插槽鏈來展開的。需要注意的是每個插槽都有自己的職責,他們各司其職完好的配合,通過一定的編排順序,來達到最終的限流降級的目的。默認的各個插槽之間的順序是固定的,因為有的插槽需要依賴其他的插槽計算出來的結果才能進行工作。

但是這并不意味著我們只能按照框架的定義來,Sentinel 通過 SlotChainBuilder 作為 SPI 接口,使得 Slot Chain 具備了擴展的能力。我們可以通過實現 SlotsChainBuilder 接口加入自定義的 slot 并自定義編排各個 slot 之間的順序,從而可以給 Sentinel 添加自定義的功能。

創建過程
com.alibaba.csp.sentinel.CtSph#lookProcessChain
可以看到,會根據當前請求的資源先去一個靜態的HashMap中獲取,如果獲取不到才會創建,創建后會保存到HashMap中。這就意味著,同一個資源會全局共享一個SlotChain

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
        ProcessorSlotChain chain = chainMap.get(resourceWrapper);
        if (chain == null) {
            synchronized (LOCK) {
                chain = chainMap.get(resourceWrapper);
                if (chain == null) {
                    // Entry size limit.
                    if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                        return null;
                    }

                    chain = SlotChainProvider.newSlotChain();
                    Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                        chainMap.size() + 1);
                    newMap.putAll(chainMap);
                    newMap.put(resourceWrapper, chain);
                    chainMap = newMap;
                }
            }
        }
        return chain;
    }
image.png

Context

我們可以看到該類的注釋
This class holds metadata of current invocation

就是說在context中維護著當前調用鏈的元數據,那元數據有哪些呢,從context類的源碼中可以看到有:
entranceNode:當前調用鏈的入口節點
curEntry:當前調用鏈的當前entry
node:與當前entry所對應的curNode
origin:當前調用鏈的調用源

它的作用
不同上下文相同名稱的資源會被分開統計

我們來看Context是怎么被使用的?
com.alibaba.csp.sentinel.slots.nodeselector.NodeSelectorSlot#entry

//1.根據context的名稱 獲取一個DefaultNode節點
DefaultNode node = map.get(context.getName());
        if (node == null) {
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    //2.雙重檢索  將資源resourceWrapper 包裝成一個DefaultNode 節點
                    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);
                }

            }
        }
        //3.設置當前節點
        context.setCurNode(node);
        //4.傳遞到下一個鏈條
        fireEntry(context, resourceWrapper, node, count, prioritized, args);

我們可以看到一點是統計節點NodeSelectorSlot是以Context的維度去統計的,而不是直接以resourceWrapper的維度去統計的?

那么為什么要這么去設計?
試想一下,如果用resourceName來做map的key,那對于同一個資源resourceA來說,在context1中獲取到的defaultNodeA和在context2中獲取到的defaultNodeA是同一個,那么怎么在這兩個context中對defaultNodeA進行更改呢,修改了一個必定會對另一個產生影響。

所以在NodeSelectorSlot這個類里面,map里面保存的是contextName和DefaultNode的映射關系,目的是為了可以在不同的context對相同的資源進行分開統計。

同一個context中對同一個resource進行多次entry()調用時,會形式一顆調用樹,這個樹是通過CtEntry之間的parent/child關系維護的。


Entry

Entry 是 Sentinel 中用來表示是否通過限流的一個憑證,就像一個token一樣。每次執行 SphU.entry() 或 SphO.entry() 都會返回一個 Entry 給調用者,意思就是告訴調用者,如果正確返回了 Entry 給你,那表示你可以正常訪問被 Sentinel 保護的后方服務了,否則 Sentinel 會拋出一個BlockException(如果是 SphO.entry() 會返回false),這就表示調用者想要訪問的服務被保護了,也就是說調用者本身被限流了。
entry中保存了本次執行 entry() 方法的一些基本信息,包括:

  • createTime:當前Entry的創建時間,主要用來后期計算rt
  • node:當前Entry所關聯的node,該node主要是記錄了當前context下該資源的統計信息
  • origin:當前Entry的調用來源,通常是調用方的應用名稱,在 ClusterBuilderSlot.entry() 方法中設置的
  • resourceWrapper:當前Entry所關聯的資源
image.png

Node

Node 中保存了資源的實時統計數據,例如:passQps,blockQps,rt等實時數據。正是有了這些統計數據后, Sentinel 才能進行限流、降級等一系列的操作。

node是一個接口,他有一個實現類:StatisticNode,但是StatisticNode本身也有兩個子類,一個是DefaultNode,另一個是ClusterNode,DefaultNode又有一個子類叫EntranceNode。

其中entranceNode是每個上下文的入口,該節點是直接掛在root下的,是全局唯一的,每一個context都會對應一個entranceNode。另外defaultNode是記錄當前調用的實時數據的,每個defaultNode都關聯著一個資源和clusterNode,有著相同資源的defaultNode,他們關聯著同一個clusterNode。


image.png
image.png

幾種Node的作用先大概介紹下:

節點 作用
StatisticNode 執行具體的資源統計操作
DefaultNode 該節點持有指定上下文中指定資源的統計信息,當在同一個上下文中多次調用entry方法時,該節點可能下會創建有一系列的子節點。另外每個DefaultNode中會關聯一個ClusterNode
ClusterNode 該節點中保存了資源的總體的運行時統計信息,包括rt,線程數,qps等等,相同的資源會全局共享同一個ClusterNode,不管他屬于哪個上下文
EntranceNode 該節點表示一棵調用鏈樹的入口節點,通過他可以獲取調用鏈樹中所有的子節點

當在一個上下文中多次調用了 SphU#entry() 方法時,就會創建一棵調用鏈樹。具體的代碼在entry方法中創建CtEntry對象時

CtEntry(ResourceWrapper resourceWrapper, ProcessorSlot<Object> chain, Context context) {
    super(resourceWrapper);
    this.chain = chain;
    this.context = context;
    // 獲取「上下文」中上一次的入口
    parent = context.getCurEntry();
    if (parent != null) {
        // 然后將當前入口設置為上一次入口的子節點
        ((CtEntry)parent).child = this;
    }
    // 設置「上下文」的當前入口為該類本身
    context.setCurEntry(this);
}

構造樹干

context初始化的時候,context中的curEntry屬性是沒有值的,如下圖所示:


image.png

創建Entry

每創建一個新的Entry對象時,都會重新設置context的curEntry,并將context原來的curEntry設置為該新Entry對象的父節點,如下圖所示:


image.png

退出Entry

某個Entry退出時,將會重新設置context的curEntry,當該Entry是最頂層的一個入口時,將會把ThreadLocal中保存的context也清除掉,如下圖所示:


image.png

構造葉子節點

上面的過程是構造了一棵調用鏈的樹,但是這棵樹只有樹干,沒有葉子,那葉子節點是在什么時候創建的呢?DefaultNode就是葉子節點,在葉子節點中保存著目標資源在當前狀態下的統計信息。通過分析,我們知道了葉子節點是在NodeSelectorSlot的entry方法中創建的。具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, Object... args) throws Throwable {
    // 根據「上下文」的名稱獲取DefaultNode
    // 多線程環境下,每個線程都會創建一個context,
    // 只要資源名相同,則context的名稱也相同,那么獲取到的節點就相同
    DefaultNode node = map.get(context.getName());
    if (node == null) {
        synchronized (this) {
            node = map.get(context.getName());
            if (node == null) {
                // 如果當前「上下文」中沒有該節點,則創建一個DefaultNode節點
                node = Env.nodeBuilder.buildTreeNode(resourceWrapper, null);
                // 省略部分代碼
            }
            // 將當前node作為「上下文」的最后一個節點的子節點添加進去
            // 如果context的curEntry.parent.curNode為null,則添加到entranceNode中去
            // 否則添加到context的curEntry.parent.curNode中去
            ((DefaultNode)context.getLastNode()).addChild(node);
        }
    }
    // 將該節點設置為「上下文」中的當前節點
    // 實際是將當前節點賦值給context中curEntry的curNode
    // 在Context的getLastNode中會用到在此處設置的curNode
    context.setCurNode(node);
    fireEntry(context, resourceWrapper, node, count, args);
}

上面的代碼可以分解成下面這些步驟:

    1. 獲取當前上下文對應的DefaultNode,如果沒有的話會為當前的調用新生成一個DefaultNode節點,它的作用是對資源進行各種統計度量以便進行流控;
    1. 將新創建的DefaultNode節點,添加到context中,作為「entranceNode」或者「curEntry.parent.curNode」的子節點;
    1. 將DefaultNode節點,添加到context中,作為「curEntry」的curNode。

我們看第3步,把當前DefaultNode設置為context的curNode,實際上是把當前節點賦值給context中curEntry的curNode,用圖形表示就是這樣:


image.png

多次創建不同的Entry,并且執行NodeSelectorSlot的entry方法后,就會變成這樣一棵調用鏈樹:


image.png

這里圖中的node0,node1,node2可能是相同的node,因為在同一個context中從map中獲取的node是同一個,這里只是為了表述的更清楚所以用了不同的節點名。

保存子節點

上面已經分析了葉子節點的構造過程,葉子節點是保存在各個Entry的curNode屬性中的。

我們知道context中只保存了入口節點和當前Entry,那子節點是什么時候保存的呢,其實子節點就是上面代碼中的第2步中保存的。

下面我們來分析上面的第2步的情況:

第一次調用NodeSelectorSlot的entry方法時,map中肯定是沒有DefaultNode的,那就會進入第2步中,創建一個node,創建完成后會把該節點加入到context的lastNode的子節點中去。我們先看一下context的getLastNode方法:

public Node getLastNode() {
    // 如果curEntry不存在時,返回entranceNode
    // 否則返回curEntry的lastNode,
    // 需要注意的是curEntry的lastNode是獲取的parent的curNode,
    // 如果每次進入的資源不同,就會每次都創建一個CtEntry,則parent為null,
    // 所以curEntry.getLastNode()也為null
    if (curEntry != null && curEntry.getLastNode() != null) {
        return curEntry.getLastNode();
    } else {
        return entranceNode;
    }
}

代碼中我們可以知道,lastNode的值可能是context中的entranceNode也可能是curEntry.parent.curNode,但是他們都是「DefaultNode」類型的節點,DefaultNode的所有子節點是保存在一個HashSet中的。

第一次調用getLastNode方法時,context中curEntry是null,因為curEntry是在第3步中才賦值的。所以,lastNode最初的值就是context的entranceNode。那么將node添加到entranceNode的子節點中去之后就變成了下面這樣:


image.png

緊接著再進入一次,資源名不同,會再次生成一個新的Entry,上面的圖形就變成下圖這樣:


image.png

此時再次調用context的getLastNode方法,因為此時curEntry的parent不再是null了,所以獲取到的lastNode是curEntry.parent.curNode,在上圖中可以很方便的看出,這個節點就是node0。那么把當前節點node1添加到lastNode的子節點中去,上面的圖形就變成下圖這樣:


image.png

然后將當前node設置給context的curNode,上面的圖形就變成下圖這樣:


image.png

假如再創建一個Entry,然后再進入一次不同的資源名,上面的圖就變成下面這樣:


image.png

至此NodeSelectorSlot的基本功能已經大致分析清楚了。

PS:以上的分析是基于每次執行SphU.entry(name)時,資源名都是不一樣的前提下。如果資源名都一樣的話,那么生成的node都相同,則只會再第一次把node加入到entranceNode的子節點中去,其他的時候,只會創建一個新的Entry,然后替換context中的curEntry的值。

NodeSelectorSlot 在執行的過程中完成了 curEntry 中 curNode 的初始化,curEntry 是在創建的時候被綁定到 context 上去的,并且在綁定的時候會添加到上一次的 entry 中去,從而形成一個鏈式結構。


ClusterBuilderSlot

NodeSelectorSlot的entry方法執行完之后,會調用fireEntry方法,此時會觸發ClusterBuilderSlot的entry方法。

ClusterBuilderSlot的entry方法比較簡單,具體代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    if (clusterNode == null) {
        synchronized (lock) {
            if (clusterNode == null) {
                // Create the cluster node.
                clusterNode = Env.nodeBuilder.buildClusterNode();
                // 將clusterNode保存到全局的map中去
                HashMap<ResourceWrapper, ClusterNode> newMap = new HashMap<ResourceWrapper, ClusterNode>(16);
                newMap.putAll(clusterNodeMap);
                newMap.put(node.getId(), clusterNode);

                clusterNodeMap = newMap;
            }
        }
    }
    // 將clusterNode塞到DefaultNode中去
    node.setClusterNode(clusterNode);

    // 省略部分代碼

    fireEntry(context, resourceWrapper, node, count, args);
}

NodeSelectorSlot的職責比較簡單,主要做了兩件事:

一、為每個資源創建一個clusterNode,然后把clusterNode塞到DefaultNode中去

二、將clusterNode保持到全局的map中去,用資源作為map的key

PS:一個資源只有一個ClusterNode,但是可以有多個DefaultNode


StatistcSlot

StatisticSlot負責來統計資源的實時狀態,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    try {
        // 觸發下一個Slot的entry方法
        fireEntry(context, resourceWrapper, node, count, args);
        // 如果能通過SlotChain中后面的Slot的entry方法,說明沒有被限流或降級
        // 統計信息
        node.increaseThreadNum();
        node.addPassRequest();
        // 省略部分代碼
    } catch (BlockException e) {
        context.getCurEntry().setError(e);
        // Add block count.
        node.increaseBlockedQps();
        // 省略部分代碼
        throw e;
    } catch (Throwable e) {
        context.getCurEntry().setError(e);
        // Should not happen
        node.increaseExceptionQps();
        // 省略部分代碼
        throw e;
    }
}

@Override
public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
    DefaultNode node = (DefaultNode)context.getCurNode();
    if (context.getCurEntry().getError() == null) {
        long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
        if (rt > Constants.TIME_DROP_VALVE) {
            rt = Constants.TIME_DROP_VALVE;
        }
        node.rt(rt);
        // 省略部分代碼
        node.decreaseThreadNum();
        // 省略部分代碼
    } 
    fireExit(context, resourceWrapper, count);
}

代碼分成了兩部分,第一部分是entry方法,該方法首先會觸發后續slot的entry方法,即SystemSlot、FlowSlot、DegradeSlot等的規則,如果規則不通過,就會拋出BlockException,則會在node中統計被block的數量。反之會在node中統計通過的請求數和線程數等信息。第二部分是在exit方法中,當退出該Entry入口時,會統計rt的時間,并減少線程數。

這些統計的實時數據會被后續的校驗規則所使用,具體的統計方式是通過 滑動窗口 來實現的。


SystemSlot

SystemSlot就是根據總的請求統計信息,來做流控,主要是防止系統被搞垮,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args)
    throws Throwable {
    SystemRuleManager.checkSystem(resourceWrapper);
    fireEntry(context, resourceWrapper, node, count, args);
}

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException {
    // 省略部分代碼
    // total qps
    double currentQps = Constants.ENTRY_NODE.successQps();
    if (currentQps > qps) {
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    }
    // total thread
    int currentThread = Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) {
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    }
    double rt = Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) {
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    }
    // 完全按照RT,BBR算法來
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) {
        if (currentThread > 1 &&
            currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) {
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        }
    }
}

其中的Constants.ENTRY_NODE是一個全局的ClusterNode,該節點的值是在StatisticsSlot中進行統計的。


AuthoritySlot

AuthoritySlot做的事也比較簡單,主要是根據黑白名單進行過濾,只要有一條規則校驗不通過,就拋出異常。

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    FlowRuleManager.checkFlow(resourceWrapper, context, node, count);
    fireEntry(context, resourceWrapper, node, count, args);
}

public static void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
    List<FlowRule> rules = flowRules.get(resource.getName());
    if (rules != null) {
        for (FlowRule rule : rules) {
            if (!rule.passCheck(context, node, count)) {
                throw new FlowException(rule.getLimitApp());
            }
        }
    }
}

DegradeSlot

DegradeSlot主要是根據前面統計好的信息,與設置的降級規則進行匹配校驗,如果規則校驗不通過則進行降級,具體的代碼如下:

@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, Object... args) throws Throwable {
    DegradeRuleManager.checkDegrade(resourceWrapper, context, node, count);
    fireEntry(context, resourceWrapper, node, count, args);
}

public static void checkDegrade(ResourceWrapper resource, Context context, DefaultNode node, int count) throws BlockException {
    List<DegradeRule> rules = degradeRules.get(resource.getName());
    if (rules != null) {
        for (DegradeRule rule : rules) {
            if (!rule.passCheck(context, node, count)) {
                throw new DegradeException(rule.getLimitApp());
            }
        }
    }
}

以上幾個概念的關系

image.png

Metric

Metric 是 Sentinel 中用來進行實時數據統計的度量接口,node就是通過metric來進行數據統計的。而metric本身也并沒有統計的能力,他也是通過Window來進行統計的。

Metric有一個實現類:ArrayMetric,在ArrayMetric中主要是通過一個叫WindowLeapArray的對象進行窗口統計的。

image.png

統計流程

image.png

Window

首先一個時間窗口是用來在某個固定時間長度內保存一些統計值的虛擬概念。有了這個概念后,我們就可以通過時間窗口來計算統計一段時間內的諸如:qps,rt,threadNum等指標了。

ArrayMetric

private final WindowLeapArray data;
 
public ArrayMetric(int windowLength, int interval) {
    this.data = new WindowLeapArray(windowLength, interval);
}

WindowLeapArray


public class WindowLeapArray extends LeapArray<Window> {
    public WindowLeapArray(int windowLengthInMs, int intervalInSec) {
        super(windowLengthInMs, intervalInSec);
    }
} 

該對象的構造方法有兩個參數:

  • windowLengthInMs :一個用毫秒做單位的時間窗口的長度
  • intervalInSec ,一個用秒做單位的時間間隔,這個時間間隔具體是做什么的,下面會分析。
    然后 WindowLeapArray 繼承自 LeapArray ,在初始化 WindowLeapArray 的時候,直接調用了父類的構造方法,再來看一下父類 LeapArray 的代碼:
public abstract class LeapArray<T> {
 
    // 時間窗口的長度
    protected int windowLength;
    // 采樣窗口的個數
    protected int sampleCount;
    // 以毫秒為單位的時間間隔
    protected int intervalInMs;
 
    // 采樣的時間窗口數組
    protected AtomicReferenceArray<WindowWrap<T>> array;
 
    /**
     * LeapArray對象
     * @param windowLength 時間窗口的長度,單位:毫秒
     * @param intervalInSec 統計的間隔,單位:秒
     */
    public LeapArray(int windowLength, int intervalInSec) {
        this.windowLength = windowLength;
        // 時間窗口的采樣個數,默認為2個采樣窗口
        this.sampleCount = intervalInSec * 1000 / windowLength;
        this.intervalInMs = intervalInSec * 1000;
 
        this.array = new AtomicReferenceArray<WindowWrap<T>>(sampleCount);
    }
}

可以很清晰的看出來在 LeapArray 中創建了一個 AtomicReferenceArray 數組,用來對時間窗口中的統計值進行采樣。通過采樣的統計值再計算出平均值,就是我們需要的最終的實時指標的值了。

可以看到我在上面的代碼中通過注釋,標明了默認采樣的時間窗口的個數是2個,這個值是怎么得到的呢?我們回憶一下 LeapArray 對象創建,是通過在 StatisticNode 中,new了一個 ArrayMetric ,然后將參數一路往上傳遞后創建的:

private transient Metric rollingCounterInSecond = new ArrayMetric(1000 / SampleCountProperty.sampleCount,IntervalProperty.INTERVAL);

SampleCountProperty.sampleCount 的默認值是2,所以第一個參數 windowLengthInMs 的值是 500ms,那么1秒鐘是1000ms,每個時間窗口的長度是500ms,也就是說總共分了兩個采樣的時間窗口。

現在繼續回到 ArrayMetric.addPass() 方法:

@Override
public void addPass() {
    WindowWrap<Window> wrap = data.currentWindow();
    wrap.value().addPass();
}

獲取當前Window
我們已經分析了 wrap.value().addPass() ,現在只需要分析清楚 data.currentWindow() 具體做了什么,拿到了當前時間窗口就可以 了。繼續深入代碼,最終定位到下面的代碼:


@Override
public WindowWrap<Window> currentWindow(long time) {
   // time每增加一個windowLength的長度,timeId就會增加1,時間窗口就會往前滑動一個
    long timeId = time / windowLength;
    // Calculate current index.
// idx被分成[0,arrayLength-1]中的某一個數,作為array數組中的索引
    int idx = (int)(timeId % array.length());
 
    // Cut the time to current window start.
    long time = time - time % windowLength;
 
    while (true) {
        //從采樣數組中根據索引獲取緩存的時間窗口
        WindowWrap<Window> old = array.get(idx);
        if (old == null) {
             // array數組長度不宜過大,否則old很多情況下都命中不了,就會創建很多個WindowWrap對象
           // 如果沒有獲取到,則創建一個新的
            WindowWrap<Window> window = new WindowWrap<Window>(windowLength, time, new Window());
            //  通過CAS將新窗口設置到數組中去
            if (array.compareAndSet(idx, null, window)) {
                // 如果能設置成功,則將該窗口返回
                return window;
            } else {
                // 否則當前線程讓出時間片,等待
                Thread.yield();
            }
       // 如果當前窗口的開始時間與old的開始時間相等,則直接返回old窗口
        } else if (time == old.windowStart()) {
            return old;
        } else if (time > old.windowStart()) {
            // 如果當前時間窗口的開始時間已經超過了old窗口的開始時間,則放棄old窗口
        // 并將time設置為新的時間窗口的開始時間,此時窗口向前滑動
            if (addLock.tryLock()) {
                try {
                    // if (old is deprecated) then [LOCK] resetTo currentTime.
                    return resetWindowTo(old, time);
                } finally {
                    addLock.unlock();
                }
            } else {
                Thread.yield();
            }
        } else if (time < old.windowStart()) {
            // Cannot go through here.
            return new WindowWrap<Window>(windowLength, time, new Window());
        }
    }
}
  • 1.根據當前時間,算出該時間的timeId,并根據timeId算出當前窗口在采樣窗口數組中的索引idx
  • 2.根據當前時間算出當前窗口的應該對應的開始時間time,以毫秒為單位
  • 3.根據索引idx,在采樣窗口數組中取得一個時間窗口old
  • 4.循環判斷知道獲取到一個當前時間窗口
  • 4.1.如果old為空,則創建一個時間窗口,并將它插入到array的第idx個位置,array上面已經分析過了,是一個 AtomicReferenceArray
  • 4.2.如果當前窗口的開始時間time與old的開始時間相等,那么說明old就是當前時間窗口,直接返回old
  • 4.3.如果當前窗口的開始時間time大于old的開始時間,則說明old窗口已經過時了,將old的開始時間更新為最新值:time,下個循環中會在步驟4.2中返回
  • 4.4.如果當前窗口的開始時間time小于old的開始時間,實際上這種情況是不可能存在的,因為time是當前時間,old是過去的一個時間

示例

image.png

初始的時候arrays數組中只有一個窗口(可能是第一個,也可能是第二個),每個時間窗口的長度是500ms,這就意味著只要當前時間與時間窗口的差值在500ms之內,時間窗口就不會向前滑動。例如,假如當前時間走到300或者500時,當前時間窗口仍然是相同的那個:


image.png

image.png

時間繼續往前走,當超過500ms時,時間窗口就會向前滑動到下一個,這時就會更新當前窗口的開始時間:


image.png

時間繼續往前走,只要不超過1000ms,則當前窗口不會發生變化:


image.png

當時間繼續往前走,當前時間超過1000ms時,就會再次進入下一個時間窗口,此時arrays數組中的窗口將會有一個失效,會有另一個新的窗口進行替換:


image.png

以此類推隨著時間的流逝,時間窗口也在發生變化,在當前時間點中進入的請求,會被統計到當前時間對應的時間窗口中。計算qps時,會用當前采樣的時間窗口中對應的指標統計值除以時間間隔,就是具體的qps。具體的代碼在StatisticNode中:

@Override
public long totalQps() {
    return passQps() + blockedQps();
}
 
@Override
public long blockedQps() {
    return rollingCounterInSecond.block() / IntervalProperty.INTERVAL;
}
 
@Override
public long passQps() {
    return rollingCounterInSecond.pass() / IntervalProperty.INTERVAL;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 為什么要回鄉掃墓?認知了清明,就懂得了人生 你好有約 昨天 文/《你好有約》 春秋時期,晉文公流亡期間,介子推曾經...
    李小訥閱讀 66評論 1 1
  • 那盤磁帶簡直要被我聽碎了。搬了幾次家,其中有一次我不在家,磁帶也就順理成章地不翼而飛了。 正式接觸周杰倫的音樂,是...
    巫馬一閱讀 145評論 0 0
  • 【愛我的奶奶和姥姥】 奶奶和姥姥都已逝去數年,近日來總是頻繁入夢。 奶奶、姥姥都是普通的農村老...
    暮色中的合歡花閱讀 397評論 0 0
  • 梨花風起,杏花飄雨 。清明節, 這個美得詩意而憂傷的古老節日, 在每個人的心里緘默成殤。 清明節禁火習俗的起源,是...
    桐桐211215閱讀 345評論 3 12