關於熔斷,一個常見的說法是,在時間視窗內,請求數達到多少,且錯誤率達到多少,就會開啟斷路器,熔斷請求。
如何計數?
假設我要統計時間視窗
一個簡單的想法,一個 int
變數 value
,請求到了 value
+ 1;一個int
變數errCt
,捕獲異常了,errCt
加一。
但是多執行緒環境下不安全啊,Sentinel爲了儘可能提升效能,沒有使用synchronized
重量級鎖,選用了LongAddr(比AtomicInteger效能更高)進行計數。
所以一個時間視窗的計數數據結構如下:
public class MetricBucket { /** * 儲存各事件的計數,比如異常總數、請求總數等; 比如counters[0]表示異常數,counters[1]表示請求總數 */ private final LongAdder[] counters; /** * 這段事件內的最小耗時 */ private volatile long minRt; }
再進一步,我要記錄前一秒,前兩秒的異常數、請求數;還要統計當前秒的異常數,緊緊一個Bucket是不夠的。
迴圈陣列
Sentinel使用一個Bucket陣列,每個元素記錄了一秒時間視窗的總請求數,異常數。如下圖:
Bucket陣列的下標定位計算方式如下:
private int calculateTimeIdx(long timeMillis) { /** * 假設當前時間戳為 1722503263235 * windowLengthInMs 為 1000 毫秒(1 秒) * 則將毫秒轉為秒 => 1722503263 * 對映到陣列的索引為 => 3 */ long timeId = timeMillis / windowLengthInMs; return (int) (timeId % array.length()); }
不是說好的迴圈陣列嗎,其實取餘的過程就是迴圈的過程。
解釋一下迴圈的意義:因為陣列長度有限,而時間每秒是無限的。當時間戳太多的時候,陣列不夠寫了,就要從陣列開頭進行覆蓋寫入。達到迴圈利用空間的效果。
MySQL的 redolog,Redis主從同步的buffer都是採用 迴圈覆蓋寫的思想。
繼續刨根問底:如何確定時間視窗是否要覆蓋?
由於迴圈使用的問題,當前時間戳與一分鐘之前的時間戳和一分鐘之後的時間戳都會對映到陣列中的同一個 Bucket,因此,必須要能夠判斷取得的 Bucket 是否是統計當前時間視窗內的指標資料,這便要陣列每個元素都儲存 Bucket 時間視窗的開始時間戳。
Sentinel直接在每個視窗都計算一個開始時間,startTime
比如當前時間戳是 1577017699235,Bucket 統計一秒的資料,將時間戳的毫秒部分全部替換為 0,就能得到 Bucket 時間視窗的開始時間戳為 1577017699000。
計算 Bucket 時間視窗的開始時間戳程式碼實現如下:
protected long calculateWindowStart(long timeMillis) { /** * 假設視窗大小為 1000 毫秒,即陣列每個元素儲存 1 秒鐘的統計數據 * timeMillis % windowLengthInMs 就是取得毫秒部分 * timeMillis - 毫秒數 = 秒部分 * 這就得到每秒的開始時間戳 */ return timeMillis - timeMillis % windowLengthInMs; }
透過startTime
就能區分是否要覆蓋了,還是當前時間就屬於這個視窗。