源碼分析 Sentinel 實時數據採集實現原理

本篇將重點關注 Sentienl 實時數據收集,即 Sentienl 具體是如何收集調用信息,以此來判斷是否需要觸發限流或熔斷。


Sentienl 實時數據收集的入口類爲 StatisticSlot。

我們先簡單來看一下 StatisticSlot 該類的註釋,來看一下該類的整體定位。

StatisticSlot,專用於實時統計的 slot。在進入一個資源時,在執行 Sentienl 的處理鏈條中會進入到該 slot 中,需要完成如下計算任務:

  • 集羣維度計算資源的總統計信息,用於集羣限流,後續文章將詳細探討。
  • 來自不同調用方/來源的羣集節點的統計信息。
  • 特定調用上下文環境的統計信息。
  • 統計所有入口的統計信息。

接下來用源碼分析的手段來詳細分析 StatisticSlot 的實現原理。

1、源碼分析 StatisticSlot

1.1 StatisticSlot entry 詳解

StatisticSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,boolean prioritized, Object... args) throws Throwable { 
	try {
		// Do some checking.
                fireEntry(context, resourceWrapper, node, count, prioritized, args);  // @1
        	// Request passed, add thread count and pass count.
        	node.increaseThreadNum();                                                             // @2
       		node.addPassRequest(count);
		if (context.getCurEntry().getOriginNode() != null) {                           // @3
			// Add count for origin node.
            		context.getCurEntry().getOriginNode().increaseThreadNum();
            		context.getCurEntry().getOriginNode().addPassRequest(count);
       		 }
		if (resourceWrapper.getEntryType() == EntryType.IN) {                // @4
			// Add count for global inbound entry node for global statistics.
            		Constants.ENTRY_NODE.increaseThreadNum();
           	 	Constants.ENTRY_NODE.addPassRequest(count);
        	}
		// Handle pass event with registered entry callback handlers.
        	for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {   // @5
            		handler.onPass(context, resourceWrapper, node, count, args);
        	}
    	} catch (PriorityWaitException ex) {                                                                                                                                // @6
		node.increaseThreadNum();
		if (context.getCurEntry().getOriginNode() != null) {
			// Add count for origin node.
            		context.getCurEntry().getOriginNode().increaseThreadNum();
       	 	}
		if (resourceWrapper.getEntryType() == EntryType.IN) {
			// Add count for global inbound entry node for global statistics.
            		Constants.ENTRY_NODE.increaseThreadNum();
        	}
        	// Handle pass event with registered entry callback handlers.
        	for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
            		handler.onPass(context, resourceWrapper, node, count, args);
        	}
    	} catch (BlockException e) {     // @7                                                                                                              
        	// Blocked, set block exception to current entry.
        	context.getCurEntry().setError(e);
		// Add block count.
        	node.increaseBlockQps(count);
        	if (context.getCurEntry().getOriginNode() != null) {
            		context.getCurEntry().getOriginNode().increaseBlockQps(count);
        	}
		if (resourceWrapper.getEntryType() == EntryType.IN) {
            		// Add count for global inbound entry node for global statistics.
            		Constants.ENTRY_NODE.increaseBlockQps(count);
        	}
        	// Handle block event with registered entry callback handlers.
        	for (ProcessorSlotEntryCallback<DefaultNode> handler : StatisticSlotCallbackRegistry.getEntryCallbacks()) {
            		handler.onBlocked(e, context, resourceWrapper, node, count, args);
        	}
		throw e;
    	} catch (Throwable e) {   // @8
        	// Unexpected error, set error to current entry.
        	context.getCurEntry().setError(e);
		// This should not happen.
        	node.increaseExceptionQps(count);
        	if (context.getCurEntry().getOriginNode() != null) {
            		context.getCurEntry().getOriginNode().increaseExceptionQps(count);
        	}
		if (resourceWrapper.getEntryType() == EntryType.IN) {
            		Constants.ENTRY_NODE.increaseExceptionQps(count);
        	}
        	throw e;
    	}
}

代碼@1:首先調用 fireEntry,先調用 Sentinel Slot Chain 中其他的處理器,執行完其他處理器的邏輯,例如 FlowSlot、DegradeSlot,因爲 StatisticSlot 的職責是收集統計信息。

代碼@2:如果後續處理器成功執行,則將正在執行線程數統計指標加一,並將通過的請求數量指標增加對應的值。下文會對 Sentinel Node 體系進行詳細的介紹,在 Sentinel 中使用 Node 來表示調用鏈中的某一個節點,每個節點關聯一個資源,資源的實時統計信息就存儲在 Node 中,故該部分也是調用 DefaultNode 的相關方法來改變線程數等,將在下文會向詳細介紹。

代碼@3:如果上下文環境中保存了調用的源頭(調用方)的節點信息不爲空,則更新該節點的統計數據:線程數與通過數量。

代碼@4:如果資源的進入類型爲 EntryType.IN,表示入站流量,更新入站全局統計數據(集羣範圍 ClusterNode)。

代碼@5:執行註冊的進入Handler,可以通過 StatisticSlotCallbackRegistry 的 addEntryCallback 註冊相關監聽器。

代碼@6:如果捕獲到 PriorityWaitException ,則認爲是等待過一定時間,但最終還是算通過,只需增加線程的個數,但無需增加節點通過的數量,具體原因我們在詳細分析限流部分時會重點討論,也會再次闡述 PriorityWaitException 的含義。

代碼@7:如果捕獲到 BlockException,則主要增加阻塞的數量。

代碼@8:如果是系統異常,則增加異常數量。

我想上面的代碼應該不難理解,但涉及到統計指標數據的變化,都是調用 DefaultNode node 相關的方法,從這裏也可以看出,Node 將是實時統計數據的直接持有者,那毋容置疑接下來將重點來學習 Node,爲了知識體系的完備性,我們先來看一下 StatisticSlot 的 exit 方法。

1.2 StatisticSlot exit 詳解

StatisticSlot#exit

public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
	DefaultNode node = (DefaultNode)context.getCurNode();
	if (context.getCurEntry().getError() == null) {         // @1
		// Calculate response time (max RT is TIME_DROP_VALVE).
		long rt = TimeUtil.currentTimeMillis() - context.getCurEntry().getCreateTime();
		if (rt > Constants.TIME_DROP_VALVE) {
			rt = Constants.TIME_DROP_VALVE;
        	}
		// Record response time and success count.
		node.addRtAndSuccess(rt, count);
		if (context.getCurEntry().getOriginNode() != null) {
			context.getCurEntry().getOriginNode().addRtAndSuccess(rt, count);
       	        }
        node.decreaseThreadNum();
	       if (context.getCurEntry().getOriginNode() != null) {
		    context.getCurEntry().getOriginNode().decreaseThreadNum();
                }
	       if (resourceWrapper.getEntryType() == EntryType.IN) {
                   Constants.ENTRY_NODE.addRtAndSuccess(rt, count);
                   Constants.ENTRY_NODE.decreaseThreadNum();
               }
        } else {
            // Error may happen.
        }
	// Handle exit event with registered exit callback handlers.
        Collection<ProcessorSlotExitCallback> exitCallbacks = StatisticSlotCallbackRegistry.getExitCallbacks();
        for (ProcessorSlotExitCallback handler : exitCallbacks) {      // @2
		handler.onExit(context, resourceWrapper, count, args);
         }
	fireExit(context, resourceWrapper, count);     // @3
}

代碼@1:成功執行,則重點關注響應時間,其實現亮點如下:
計算本次響應時間,將本次響應時間收集到 Node 中。
將當前活躍線程數減一。

代碼@2:執行退出時的 callback。可以通過 StatisticSlotCallbackRegistry 的 addExitCallback 方法添加退出回調函數。

代碼@3:傳播 exit 事件。

接下來我們將重點介紹 DefaultNode,即 Sentinel 的 Node 體系,持有資源的實時調用信息。

2、Sentienl Node 體系

2.1 Node 類體系圖
在這裏插入圖片描述
我們先簡單介紹一下上述核心類的作用與核心接口或核心屬性的含義。

  • OccupySupport
    支持搶佔未來的時間窗口,有點類似借用“未來”的令牌。其核心方法如下:
    • long tryOccupyNext(long currentTime, int acquireCount, double threshold)
      嘗試搶佔未來的令牌,返回值爲調用該方法的線程應該 sleep 的時間。
      1、long currentTime
      當前時間。
      2、int acquireCount
      本次需要申請的令牌個數。
      3、double threshold
      設置的闊值。
  • long waiting()
    獲取當前已申請的未來的令牌的個數。
  • void addWaitingRequest(long futureTime, int acquireCount)
    申請未來時間窗口中的令牌。
  • void addOccupiedPass(int acquireCount)
    增加申請未來令牌通過的個數。
  • double occupiedPassQps()
    當前搶佔未來令牌的QPS。
  • Node
    持有實時統計信息的節點。定義了收集統計信息與獲取統計信息的接口,上面方法根據方法名稱即可得知其含義,故這裏就不一一羅列了。
  • StatisticNode
    實現統計信息的默認實現類。
  • DefaultNode
    用於在特定上下文環境中保存某一個資源的實時統計信息。
  • ClusterNode
    實現基於集羣限流模式的節點,將在集羣限流模式部分詳細介紹。
  • EntranceNode
    用來表示調用鏈入口的節點信息。

本文將詳細介紹 DefaultNode 與 StatisticNode,重點闡述調用樹與實時統計信息。DefaultNode 是 StatisticNode 的子類,我們先從 StatisticNode 開始 Node 體系的探究。

2、StatisticNode 詳解

2.1 核心類圖

在這裏插入圖片描述
我們對其核心屬性進行一一解讀:

  • Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL)
    每秒的實時統計信息,使用 ArrayMetric 實現,即基於滑動窗口實現,正是上篇文章詳細介紹的,默認1s 採樣 2次。即一個統計週期中包含兩個滑動窗口。
  • Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false)
    每分鐘實時統計信息,同樣使用 ArrayMetric 實現,即基於滑動窗口實現。每1分鐘,抽樣60次,即包含60個滑動窗口,每一個窗口的時間間隔爲 1s 。
  • LongAdder curThreadNum = new LongAdder()
    當前線程計數器。
  • long lastFetchTime = -1
    上一次獲取資源的有效統計數據的時間,即調用 Node 的 metrics() 方法的時間。

關於 ArrayMetric 滑動窗口設計與實現原理,請參考筆者的另一篇博文:Alibaba Seninel 滑動窗口實現原理(文末附原理圖)

接下來我們挑選幾個具有代表性的方法進行探究。

2.2 addPassRequest

public void addPassRequest(int count) {
	rollingCounterInSecond.addPass(count);
	rollingCounterInMinute.addPass(count);
}

增加通過請求數量。即將實時調用信息向滑動窗口中進行統計。addPassRequest 即報告成功的通過數量。就是分別調用 秒級、分鐘即對應的滑動窗口中添加數量,然後限流規則、熔斷規則將基於滑動窗口中的值進行計算。

2.3 totalRequest

public long totalRequest() {
	return rollingCounterInMinute.pass() + rollingCounterInMinute.block();
}

獲取當前時間戳的總請求數,獲取分鐘級時間窗口中的統計信息。

2.4 successQps

public double successQps() {
	return rollingCounterInSecond.success() / rollingCounterInSecond.getWindowIntervalInSec();
}

成功TPS,用秒級統計滑動窗口中統計的個數 除以 窗口的間隔得出其 tps,即抽樣個數越大,其統計越精確。

溫馨提示:上面的方法在學習了上文的滑動窗口設計原理後將顯得非常簡單,大家在學習的過程中,可以總結出一個規律,什麼時候時候使用秒級滑動窗口,什麼時候使用分鐘級滑動窗口。

2.5 metrics

由於 Sentienl 基於滑動窗口來實時收集統計信息,並存儲在內存中,並隨着時間的推移,舊的滑動窗口將失效,故需要提供一個方法,及時將所有的統計信息進行彙總輸出,供監控客戶端定時拉取,轉儲都其他客戶端,例如數據庫,方便監控數據的可視化,這也通常是中間件用於監控指標的監控與採集的通用設計方法。

public Map<Long, MetricNode> metrics() {
    long currentTime = TimeUtil.currentTimeMillis();
    currentTime = currentTime - currentTime % 1000;   // @1
    Map<Long, MetricNode> metrics = new ConcurrentHashMap<>();
    List<MetricNode> nodesOfEverySecond = rollingCounterInMinute.details();   // @2
    long newLastFetchTime = lastFetchTime;
    // Iterate metrics of all resources, filter valid metrics (not-empty and up-to-date).
    for (MetricNode node : nodesOfEverySecond) { 
        if (isNodeInTime(node, currentTime) && isValidMetricNode(node)) {    // @3
	    metrics.put(node.getTimestamp(), node);
            newLastFetchTime = Math.max(newLastFetchTime, node.getTimestamp());
        }
    }
    lastFetchTime = newLastFetchTime;
    return metrics;
}

代碼@1:獲取當前時間對應的滑動窗口的開始時間,可以對比上文計算滑動窗口的算法。

代碼@2:獲取一分鐘內的所有滑動窗口中的統計數據,使用 MetricNode 表示。

代碼@3:遍歷所有節點,刷選出不是當前滑動窗口外的所有數據。這裏的重點是方法:isNodeInTime。

private boolean isNodeInTime(MetricNode node, long currentTime) {
    return node.getTimestamp() > lastFetchTime && node.getTimestamp() < currentTime;
}

這裏只刷選出不是當前窗口的數據,即 metrics 方法返回的是“過去”的統計數據。

接下來我們再來看看 DefaultNode 相關的幾個特性方法。

3、DefaultNode 詳解

3.1 類圖

在這裏插入圖片描述
DefaultNode 是 StatisticNode 的子類,其額外增加的屬性如下:

  • private ResourceWrapper id
    資源id,即 DefaultNode 才真正與資源掛鉤,可以將 DefaultNode 看出是調用鏈中的一個節點,並且與資源關聯。
  • private volatile Set< Node > childList
    子節點結合。以此來維持其調用鏈。
  • private ClusterNode clusterNode
    集羣節點,同樣爲 StatisticNode 的子類,表示與資源集羣相關的環境。

接下來我們將來看一下 DefaultNode 的核心方法。

3.2 increaseBlockQps

public void increaseBlockQps(int count) {
    super.increaseBlockQps(count);
    this.clusterNode.increaseBlockQps(count);
}

DefaultNode 的此類方法,通常是先調用 StatisticNode 的方法,然後再調用 clusterNode 的相關方法,最終就是使用在對應的滑動窗口中增加或減少計量值。

其他方法也比較簡單,就不再細看了,我們可以通過 DefaultNode 的 printDefaultNode 方法來打印該節點的調用鏈。

本文就介紹到這裏了,本文詳細介紹了 Sentinel 實時數據收集的統一入口 StatisticSlot,並且介紹了 Seninel Node 體系,即調用鏈中的每一個節點,每一個節點對一個資源的實時統計信息。下一篇將開始重點限流是如何實現的,即 FlowSlot 的實現技巧。


歡迎加筆者微信號(dingwpmz),加羣探討,筆者優質專欄目錄:
1、源碼分析RocketMQ專欄(40篇+)
2、源碼分析Sentinel專欄(12篇+)
3、源碼分析Dubbo專欄(28篇+)
4、源碼分析Mybatis專欄
5、源碼分析Netty專欄(18篇+)
6、源碼分析JUC專欄
7、源碼分析Elasticjob專欄
8、Elasticsearch專欄(20篇+)
9、源碼分析MyCat專欄

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章