Sentinel源码三:滑动窗口

文章详细阐述了Sentinel如何利用滑动窗口算法进行流量控制,包括原理、优势以及其实现细节,如LeapArray和MetricBucket在统计和管理流量数据中的作用。
摘要由CSDN通过智能技术生成

为什么使用滑动窗口

Sentinel 使用滑动窗口算法来进行限流是为了实现更精细和实时的流量控制。这种方法允许系统更灵活、更精确地处理突发流量,保证资源的合理分配,并且降低了服务因流量突增而超载的风险。下面详细解释 Sentinel 使用滑动窗口进行限流的原理和优势。

image.png

滑动窗口原理

滑动窗口是一种常用的算法,用于统计一段时间内的事件或数据点。在限流场景中,滑动窗口通过将时间窗口分割成多个小的时间片段(通常称为桶),每个时间片段独立统计进入的请求量。随着时间的推移,最旧的时间片段的数据会被新的时间片段替换,形成“滑动”的效果。

实现细粒度的时间控制

使用滑动窗口算法可以更精细地控制时间窗口内的流量。与固定窗口(整个时间窗口只统计一次)相比,滑动窗口通过连续滑动减少了窗口切换时的流量突变,避免了请求在窗口刚开始时因为累积的计数而被误判为超限。

减少突发流量的影响

在实际应用中,流量往往呈现出突发性特征。如果使用固定窗口算法,在窗口重置的瞬间可能会接受大量请求,造成短时间内的服务压力。滑动窗口通过平滑时间窗口的边缘,可以更均匀地分布请求,从而降低了因突发请求导致的系统压力。

提高系统响应的实时性

滑动窗口提供了实时更新的流量数据,使得系统能够基于最近的流量情况做出快速响应。这对于需要快速适应流量变化的在线服务尤其重要,可以即时调整资源分配和访问策略。

保证服务的稳定性和可靠性

通过精确控制每个时间片段内的流量,滑动窗口算法帮助确保了服务的稳定性和可靠性,避免因超负载而导致的服务不可用或响应延迟增加。

Sentinel是如何实现的

滑动窗口LeapArray

image.png
�对应的是这个:
image.png

按照图上那就是 样本窗口为10s,样本窗口数为6,以秒为单位的滑动窗口跨越时间长度为60s(1分钟),里面有多个WindowWrap(样本窗口)

样本窗口

image.png
对应的是这个:
image.png
窗口长度为10s,窗口开始时间计算出来的,后面会分析到。窗口数据为MetricBucket,这个窗口数据存储一些数据:比如通过的请求多少、阻塞多少、成功了多少等等

MetricBucket

MetricBucket 是用于存储度量数据的基本数据结构。它是性能监控和流量统计的核心组成部分,用于记录各种运行时的指标。每个 MetricBucket 通常包括以下几种类型的计数器,这些计数器通过 LongAdder 类型来实现,以支持高并发下的快速更新。
image.png
每个LongAdder你可以理解为存储一类的数据:

  1. 通过请求数(Passed Requests)
    • 记录在监控窗口内成功通过 Sentinel 检查的请求数量。这包括所有没有被限流或降级的请求。
  2. 阻塞请求数(Blocked Requests)
    • 记录因达到预定义的限流条件而被 Sentinel 阻塞的请求数量。这些是那些被限流规则阻止执行的请求。
  3. 成功执行的请求数(Successful Requests)
    • 记录在监控窗口内成功执行并正常完成的请求数量。这些请求没有在执行过程中抛出任何异常。
  4. 异常请求数(Exception Requests)
    • 记录在执行过程中抛出异常的请求数量。这些是因服务异常(如抛出异常)而未成功完成的请求。
  5. 响应时间(Response Time)
    • 累加每个请求的处理时间,用于计算在监控窗口内请求的平均响应时间。这有助于监控服务的性能表现。

��

源码剖析

入口:StatisticSlot

image.png
在之前分析StatisticSlot讲过,它会往后执行,等执行完后会执行到这里的node.addPassRequest(count);
image.png

�DefaultNode.addPassRequest

image.png
这里面有两个滑动窗口,一个是秒级别的滑动窗口(1秒的滑动窗口、 500ms一个样本窗口,2个样本),一个是分钟级别的滑动窗口(1分钟的滑动窗口、1秒一个样本窗口,60个样本)

我们就以rollingCounterInSecond.addPass来分析,逻辑都是一样的

rollingCounterInSecond.addPass

image.png

这里会调用到LeapArray的currentWindow方法:

 public WindowWrap<T> currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        // todo 比如timeMillis为888ms,现在用的是按1s统计的,2个样本窗口,一个500ms
        // 根据时间戳获取时间窗口索引值
        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.
         */
        while (true) {
            // 尝试获取已有的时间窗口
            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 就是对时间窗口的抽象类,真正的时间窗口存储结构是MetricBucket
                WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                // 更新时间窗口到LeapArray中
                // LeapArray是对整个滑动时间窗口集实际存储,整个滑动时间窗口存在于其中的
                // AtomicReferenceArray<WindowWrap<T>> array
                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()) {
                // 时钟回拨问题,例如服务器时间被前调,导致了计算出来的窗口开始时间小于了现在目标的窗口时间
                // 那么就新建一个窗口,仅用作统计,不会在流控slot中进行计算,出现这个问题肯定就会计算不准
                // Should not go through here, as the provided time is already behind.
                return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }

代码比较多,一点一点分析:

  1. 获取样本窗口的索引值:(拿当前的时间戳/样本窗口时间) % 样本窗口长度

image.png
比如这里的timeMillis为1714292771261,
timeMillis/500 获取到3428585542
3428585542 % 2最终得到样本窗口索引为0

  1. 获取时间窗口的开始时间

image.png
这个比较简单,拿timeMillis - timeMillis % windowLengthInMs 获取在该样本窗口的起始位置

  1. 看当前index 有没有样本窗口,如果没有就需要创建一个

image.png

  1. 如果已经存在就需要判断 当前计算出来的窗口开始时间 == 存在窗口的开始时间,如果等于,就直接返回,

image.png

  1. 如果不等于(说明之前的窗口数据已经过期了),就会出现窗口滑动,重新设置一个窗口

image.png

更新窗口统计数据:wrap.value().addPass(count)

image.png
这个value就是MetricBucket�
image.png

会通过这个event的original值定位到counters下标,然后做 +1操作
image.png

简单总结

本篇主要还是介绍了滑动窗口进行一个qps等数据的保存和维护的,后续我们将进入flowSlot的源码分析,看看限流是如何运用刚刚统计到的信息进行一个限流判定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值