Sentinel系列之滑动窗口、漏桶代码分析

1. 滑动窗口

  • 原理

    滑动窗口限流算法(Sliding Window)是对固定窗口算法的一个改进。在滑动窗口算法中,窗口大小仍然是固定的,但它把单位时间周期划分为n个小周期,分别记录每个小周期内请求的数量,根据时间滑动删除过期的小周期。

    需要注意的是,当请求到达新的周期,才会往前滑动,也就是说滑动是在过了一个完整的窗口之后才开始的,不是立即开始,否则和固定窗口没什么区别。

image

  • Sentinel实现

    StatisticNode使用了滑动窗口算法统计指标,在DefaultController中被调用,作为默认的一种限流方式。(com.alibaba.csp.sentinel.slots.block.flow.TrafficShapingController是Sentinel的流量控制算法类,DefaultController是它的一个实现)

image

StatisticNode持有两个ArrayMetric对象,分别是以秒为单位和以分为单位的时间窗口。限流判断用的是第一个窗口,它是1秒钟的窗口,被分成了2个小周期。

image

ArrayMetric持有一个LeapArray对象,LeapArray是Sentinel里面一个指标度量的基类。

它拥有一个数组,protected final AtomicReferenceArray<WindowWrap> array; 存储一个个小周期的桶(Bucket),记录时间周期内请求的数据。

它有两种实现,我们先看BucketLeapArray.

  • BucketLeapArray

    image

    它的一个核心方法是com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow(),在很多操作中第一步都是定位到当前的窗口周期。

    具体实现如下:

    a. 根据传入时间(一般是当前时间)计算出对应窗口在数组中的下标和当前窗口开始的时间

    image

    b. 取出桶元素,分三种场景进行讨论

    image

    c. 如果桶不存在,直接创建并CAS设置。

    image

    d. 如果桶存在且开始时间相相等,说明桶没有过期,直接使用

    image

    e. 桶已经过期了,要清空并重新设置窗口开始时间。没有专门线程清理过期的桶。超过一个窗口之后,会重新从下标0开始填充。

    image

  • OccupiableBucketLeapArray

    Sentinel 1.5.0 对底层的滑动窗口统计结构进行了升级,添加了“占用”机制,允许在当前 QPS 已经达到限流阈值时,同个资源高优先级的请求提前占用未来时间窗口的配额数,等待到对应时间窗口到达时直接通过,从而可以实现“最终通过”的效果而不是被立即拒绝;而同个资源低优先级的请求则不能占用未来的配额,阈值达到时就会被限流。

    Sentinel 1.5.0 引入了 FutureBucketLeapArray,这是一种特殊的滑动窗口,仅维持当前时间以后的格子,从而可以用于统计未来被预先占用的配额数目。Sentinel 将普通的滑动窗口与 FutureBucketLeapArray 组合成可占用的滑动窗口 OccupiableBucketLeapArray,从而实现了“部分高优先级请求最终通过”的效果。

    https://sentinelguard.io/zh-cn/blog/sentinel-1-5-0-release.html

  • 优点

    简单易懂,精度高。

  • 缺点

    一旦到达限流后,请求都会直接暴力被拒绝,不够友好。

2. 漏桶

  • 原理

    漏桶算法(Leaky Bucket)是将请求放入一个有固定容量的“桶”中,桶内的请求以固定速率传出。当桶满时,新进入的请求将被丢弃。

    漏桶算法可以保证处理请求的速率恒定,从而有效防止流量激增导致的服务不稳定,有点类似生产者消费者模式。

    image

  • Sentinel实现

    Sentinel在com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController通过漏桶算法实现了匀速处理,也就是4.2的模式。

    这个模式的难点主要有两个:1.如何实现匀速处理 2.如何实现排队

    对于第1点,Sentinel的实现方式是,把一个时间周期(一般是1秒)按允许通过的请求数,平均分成N个小区间,一个小区间内只允许有一个请求通过。

    也就是说,每个请求可以去抢占一个小区间,如果抢到了并且不用等待超过最大等待时间,那就排队,否则拒绝。而资源端每个小区间只会处理一个请求,在一个时间周期内处理的数量也是恒定的,这就达到了匀速的目的。

    对于第2点,Sentinel是使用了Thread.sleep()让线程休眠达到排队效果。

    具体步骤如下:

    a.使用一个字段latestPassedTime来记录当前请求已经发放到哪一个时间点

    b.在每次请求到来之时,先计算它需要消耗的时间costTime(即一个小区间的大小)

    c. latestPassedTime+costTime得出预期时间expectedTime

    d. expectedTime-currentTime得出等待时间waitTime

    e. 如果waitTime小于等于允许等待时间,则排队,否则直接拒绝

    需要注意的是,latestPassedTime不是指已经通过了请求的时间点,不一定是过去的时间,它表示的是当前被抢占的区间已经到哪个时间点了,请求可以抢占未来的区间。

    image

源码分析

image

posted @ 2023-10-04 17:54  kingsleylam  阅读(570)  评论(0编辑  收藏  举报