Spring Cloud Alibaba Sentinel 滑动窗口

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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值