1、构建滑动窗口
在前面构建处理器链的时候创建了一个StatisticNode。
用数组实现,保存资源的静态实时数据。
保存了三种类型的实时静态监控数据:秒级别监控,分钟级别监控和线程数量。
public class StatisticNode implements Node {
/**
* Holds statistics of the recent {@code INTERVAL} seconds. The {@code INTERVAL} is divided into time spans
* by given {@code sampleCount}.
*/
//SAMPLE_COUNT = 2
//初始化有两个桶
//INTERVAL = RuleConstant.DEFAULT_WINDOW_INTERVAL_MS = 1000
//滚动计数器,秒
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
/**
* Holds statistics of the recent 60 seconds. The windowLengthInMs is deliberately set to 1000 milliseconds,
* meaning each bucket per second, in this way we can get accurate statistics of each second.
*/
//滚动计数器,分 60个桶,60秒 初始化的是BucketLeapArray
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
/**
* The counter for thread count.
*/
//当前线程的计数 LongAdder线程安全的long
private LongAdder curThreadNum = new LongAdder();
/**
* The last timestamp when metrics were fetched.
*/
//最后拉取的时间 时间戳
private long lastFetchTime = -1;
//监控,由MetricTimerListener调用
...
}
1.1、秒计数器
//桶数组
private final LeapArray<MetricBucket> data;
public ArrayMetric(int sampleCount, int intervalInMs) {
//2,1000
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
每秒钟两个窗口,每个窗口500毫秒
//可以透支的桶
//OccupiableBucketLeapArray extends LeapArray<MetricBucket>
private final FutureBucketLeapArray borrowArray;
public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
// This class is the original "CombinedBucketArray".
//2,1000
super(sampleCount, intervalInMs);
//可以预支两个桶
this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
}
OccupiableBucketLeapArray继承了LeapArray
//每个窗口的长度 毫秒
protected int windowLengthInMs;
//窗口数
protected int sampleCount;
//间隔 毫秒
protected int intervalInMs;
//间隔 秒
private double intervalInSecond;
//滑动窗口数组
protected final AtomicReferenceArray<WindowWrap<T>> array;
private final ReentrantLock updateLock = new ReentrantLock();
/**
* The total bucket count is: {@code sampleCount = intervalInMs / windowLengthInMs}.
*
* @param sampleCount bucket count of the sliding window
* @param intervalInMs the total time interval of this {@link LeapArray} in milliseconds
*/
public LeapArray(int sampleCount, int intervalInMs) {
//校验省略
...
//500
this.windowLengthInMs = intervalInMs / sampleCount;
//1000
this.intervalInMs = intervalInMs;
//1
this.intervalInSecond = intervalInMs / 1000.0;
//2
this.sampleCount = sampleCount;
this.array = new AtomicReferenceArray<>(sampleCount);
}
//FutureBucketLeapArray extends LeapArray<MetricBucket>
public FutureBucketLeapArray(int sampleCount, int intervalInMs) {
// This class is the original "BorrowBucketArray". 2,1000
super(sampleCount, intervalInMs);
}
1.2、分计数器
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
if (enableOccupy) {
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}
一分钟60的窗口,每个窗口1000毫秒
//BucketLeapArray extends LeapArray<MetricBucket>
public BucketLeapArray(int sampleCount, int intervalInMs) {
//60,60*1000 一分钟60个桶,桶长度1000毫秒
super(sampleCount, intervalInMs);
}
BucketLeapArray也继承了LeapArray
2、获取当前的窗口
前面statisticSlot中调用addPassRequest方法增加通过请求数量。
//com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow()
public WindowWrap<T> currentWindow() {
return currentWindow(TimeUtil.currentTimeMillis());
}
获取当前时间的窗口
/**
* Get bucket item at provided timestamp.
*
* @param timeMillis a valid timestamp in milliseconds
* @return current bucket item at provided timestamp if the time is valid; null if time is invalid
*/
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
//获取窗口下标
int idx = calculateTimeIdx(timeMillis);
// Calculate current bucket start time.
//获取窗口开始时间
long windowStart = calculateWindowStart(timeMillis);
/*
* Get bucket item at given time from the array.
*
* (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
* (2) Bucket is up-to-date, then just return the bucket.
* (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
*/
while (true) {
//从数组中获取一个窗口,bucket
WindowWrap<T> old = array.get(idx);
//当前时间窗口不存在,创建一个并设置到环形数组
if (old == null) {
/*
* B0 B1 B2 NULL B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
* time=888
* bucket is empty, so create new and update
*
* If the old bucket is absent, then we create a new bucket at {@code windowStart},
* then try to update circular array via a CAS operation. Only one thread can
* succeed to update, while other threads yield its time slice.
*/
//创建窗口并设置到环形数组
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
if (array.compareAndSet(idx, null, window)) {
// Successfully updated, return the created bucket.
return window;
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
//这里因为会出现多线程并发修改的情况
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
//当前窗口存在,返回当前窗口
/*
* B0 B1 B2 B3 B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
* time=888
* startTime of Bucket 3: 800, so it's up-to-date
*
* If current {@code windowStart} is equal to the start timestamp of old bucket,
* that means the time is within the bucket, so directly return the bucket.
*/
return old;
} else if (windowStart > old.windowStart()) {
//丢弃,超过了原来时间窗口的统计周期,丢弃原来的,重新设置新的
/*
* (old)
* B0 B1 B2 NULL B4
* |_______||_______|_______|_______|_______|_______||___
* ... 1200 1400 1600 1800 2000 2200 timestamp
* ^
* time=1676
* startTime of Bucket 2: 400, deprecated, should be reset
*
* If the start timestamp of old bucket is behind provided time, that means
* the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
* Note that the reset and clean-up operations are hard to be atomic,
* so we need a update lock to guarantee the correctness of bucket update.
*
* The update lock is conditional (tiny scope) and will take effect only when
* bucket is deprecated, so in most cases it won't lead to performance loss.
*/
if (updateLock.tryLock()) {
try {
// Successfully get the update lock, now we reset the bucket.
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
// Contention failed, the thread will yield its time slice to wait for bucket available.
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
//当前时间比窗口时间小,错误的,直接返回一个新的窗口
// Should not go through here, as the provided time is already behind.
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
2.1、获取当前时间的窗口下标
protected int windowLengthInMs;
protected int sampleCount;
protected int intervalInMs;
private double intervalInSecond;
protected final AtomicReferenceArray<WindowWrap<T>> array;
private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
//比如时间窗口长度是1000ms,则算出来有多少个1000ms,不满1000的都在这个窗口里面
long timeId = timeMillis / windowLengthInMs;
// Calculate current index so we can map the timestamp to the leap array.
//取模,找到对应的窗口
return (int)(timeId % array.length());
}
上面的比如是秒的计数器,timedId = xxx / 500, 算出来有几个500,然后在对滑动窗口的长度取模,找到在滑动窗口的哪个下标。
2.2、获取窗口的时间的开始时间
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
//将当前时间不满一个窗口的时间去掉,实际就是该窗口0时间
return timeMillis - timeMillis % windowLengthInMs;
}
上面的比如 550 - 550 % 500 = 500,这个是一个桶的窗口的起始毫秒数,0499,500999。。。
上面两个算法算出来就可以达到一个环形数组的效果,因为数组下标达到最大后又会归零。
2.3、创建空的桶
//com.alibaba.csp.sentinel.slots.statistic.metric.BucketLeapArray#newEmptyBucket
public MetricBucket newEmptyBucket(long time) {
return new MetricBucket();
}
private final LongAdder[] counters;
private volatile long minRt;
public MetricBucket() {
MetricEvent[] events = MetricEvent.values();
this.counters = new LongAdder[events.length];
for (MetricEvent event : events) {
counters[event.ordinal()] = new LongAdder();
}
initMinRt();
}
public enum MetricEvent {
/**
* Normal pass.
*/
PASS,
/**
* Normal block.
*/
BLOCK,
EXCEPTION,
SUCCESS,
RT,
/**
* Passed in future quota (pre-occupied, since 1.5.0).
*/
OCCUPIED_PASS
}
这个桶里面创建了LongAdder数组,是个线程安全的Long。
private void initMinRt() {
this.minRt = SentinelConfig.statisticMaxRt();
}
private static final Map<String, String> props = new ConcurrentHashMap<>();
//STATISTIC_MAX_RT = "csp.sentinel.statistic.max.rt";
//com.alibaba.csp.sentinel.config.SentinelConfig#statisticMaxRt
public static int statisticMaxRt() {
String v = props.get(STATISTIC_MAX_RT);
try {
if (StringUtil.isEmpty(v)) {
//DEFAULT_STATISTIC_MAX_RT = 5000;
return DEFAULT_STATISTIC_MAX_RT;
}
return Integer.parseInt(v);
} catch (Throwable throwable) {
RecordLog.warn("[SentinelConfig] Invalid statisticMaxRt value: {}, using the default value instead: "
+ DEFAULT_STATISTIC_MAX_RT, v, throwable);
SentinelConfig.setConfig(STATISTIC_MAX_RT, String.valueOf(DEFAULT_STATISTIC_MAX_RT));
return DEFAULT_STATISTIC_MAX_RT;
}
}
2.4、创建新的窗口
//窗口长度,毫秒数
private final long windowLengthInMs;
//窗口开始的毫秒数
private long windowStart;
//窗口的值 上面创建的那个桶
private T value;
public WindowWrap(long windowLengthInMs, long windowStart, T value) {
this.windowLengthInMs = windowLengthInMs;
this.windowStart = windowStart;
this.value = value;
}
3、重置窗口
//com.alibaba.csp.sentinel.slots.statistic.metric.BucketLeapArray#resetWindowTo
protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long startTime) {
// Update the start time and reset value.
w.resetTo(startTime);
w.value().reset();
return w;
}
3.1、重置窗口开始时间
//com.alibaba.csp.sentinel.slots.statistic.base.WindowWrap#resetTo
public WindowWrap<T> resetTo(long startTime) {
this.windowStart = startTime;
return this;
}
3.2、重置窗口的计数
//com.alibaba.csp.sentinel.slots.statistic.data.MetricBucket#reset()
public MetricBucket reset() {
//将窗口的所有事件的计数重置
for (MetricEvent event : MetricEvent.values()) {
//将对应事件的计数器重置
counters[event.ordinal()].reset();
}
initMinRt();
return this;
}