前言:
Sentinel 的一个重要功能就是限流,对于限流来说有多种的限流算法,比如滑动时间窗口算法、漏桶算法、令牌桶算法等,Sentinel 对这几种算法都有具体的实现,如果我们对某一个资源设置了一个流控规则,并且选择的流控模式是“快速失败”,那么 Sentinel 就会采用滑动时间窗口算法来作为该资源的限流算法,本篇我们来分析 Sentinel 中滑动时间窗口算法的实现。
Sentinel 系列文章传送门:
Spring Cloud 整合 Nacos、Sentinel、OpenFigen 实战【微服务熔断降级实战】
Sentinel 源码分析入门【Entry、Chain、Context】
Sentine 源码分析之–NodeSelectorSlot、ClusterBuilderSlot、StatisticSlot
Sentine 源码分析之–AuthoritySlot、SystemSlot、GatewayFlowSlot
滑动窗口的原理
滑动窗口是一种常用的算法,用于统计一段时间内的事件或数据点,在限流场景中,滑动窗口将时间窗口分割成多个小的时间片段(通常称为桶,也可以叫做样本窗口),每个时间片段独立统计,随着时间的推移,最旧的时间片段的数据会被新的时间片段替换,形成“滑动”的效果,在具体实现上,滑动时间窗算法可以通过多种数据结构来实现,例如使用环形数组、哈希表等,可以使用一个环形数组来存储时间窗口内的数据点,数组的大小等于时间窗口的大小,每当有新的数据点进入时,旧的对应时间点的数据将被覆盖,从而实现滑动时间窗的效果,此外,也可以使用哈希表结构来实现滑动时间窗口,其中键为时间点,值为该时间点的数据值或变化量。
滑动窗口的优点
- 实现更细粒度的时间控制,与固定窗口(整个时间窗口只统计一次)相比,滑动窗口通过连续滑动减少了窗口切换时的流量突变,避免了请求在窗口刚开始时因为累积的计数而被误判为超限。
- 减少突发流量对系统的影响,保证服务的稳定性和可靠性,在实际应用中,流量往往呈现出突发性特征,如果使用固定窗口算法,在窗口重置的瞬间可能会接受大量请求(时间窗口的起始点聚集大量流量),造成短时间内的服务压力,滑动窗口可以更均匀、更细粒度的控制每个时间片段内的流量,从而降低了因突发流量导致的导致的系统压力。
- 提高系统响应的实时性,滑动窗口提供了更实时的流量数据,系统能够基于最实时的流量情况做出响应,这对于需要快速适应流量变化的在线服务尤其重要,可以即时调整资源分配和访问策略。
Sentinel 滑动时间窗口的实现
Sentinel 官网的图就很清楚的告诉了我们 Sentinel 使用环形数组实现滑动窗口,下图中的右上角就是滑动窗口的示意图,是 StatisticSlot 的具体实现,底层采用的是 LeapArray 来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。
滑动窗口的核心数据结构
- ArrayMetric:滑动窗口核心实现类。
- LeapArray:滑动窗口顶层数据结构,主要存储窗口数据。
- WindowWrap:每一个滑动窗口的包装类,其内部的数据结构用 MetricBucket 表示。
- MetricBucket:指标桶,例如通过数量、阻塞数量、异常数量、成功数量、响应时间,已通过未来配额(抢占下一个滑动窗口的数量)。
- MetricEvent:指标类型,例如通过数量、阻塞数量、异常数量、成功数量、响应时间等。
ArrayMetric 构造方法源码解析
ArrayMetric 是滑动窗口的入口类,实现了 Metric 接口,该接口主要定义一个滑动窗口中成功的数量、异常数量、阻塞数量,TPS、响应时间等,ArrayMetric 提供了两个构造方法,两个构造方法的区别是在于当前时间窗口达到限制之后,是否可以抢占下一个时间窗口,具体逻辑如下:
- intervalInMs:滑动窗口的总时间,例如 1 分钟、1 秒中。
- sampleCount:在一个滑动窗口的总时间中的抽样的个数,默认为 2,即一个滑动窗口的总时间包含两个相等的区间,一个区间就是一个窗口。
- enableOccupy:是否允许抢占,即当前滑动窗口已经达到限制后,是否可以占用下一个时间窗口的容量。
public class ArrayMetric implements Metric {
private final LeapArray<MetricBucket> data;
//com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#ArrayMetric(int, int)
public ArrayMetric(int sampleCount, int intervalInMs) {
//默认是可抢占的
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
}
//com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#ArrayMetric(int, int, boolean)
public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
//当前时间窗口容量满了 是否可抢占时间窗口
if (enableOccupy) {
//可抢占
this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
} else {
//不可抢占
this.data = new BucketLeapArray(sampleCount, intervalInMs);
}
}
}
LeapArray 源码分析
LeapArray 用来存储滑动窗口数据的,也就是所谓的环形数组,具体成员变量如下:
- windowLengthInMs:样本窗口的时间间隔,单位秒。
- sampleCount:样本窗口数量。
- intervalInMs:一个滑动窗口跨越的时间长度,也就是总时间窗口。
- array:样本窗口的集合,使用 AtomicReferenceArray 保证原子性。
public abstract class LeapArray<T> {
//样本窗口的时间间隔 单位秒
protected int windowLengthInMs;
//样本窗口数量
protected int sampleCount;
//毫秒为单位 一个滑动窗口跨越的时间长度 也就是总时间窗口
protected int intervalInMs;
//样本窗口的集合 使用 AtomicReferenceArray 保证原子性
protected final AtomicReferenceArray<WindowWrap<T>> array;
/**
* The conditional (predicate) update lock is used only when current bucket is deprecated.
*/
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) {
AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");
this.windowLengthInMs = intervalInMs / sampleCount;
this.intervalInMs = intervalInMs;
this.sampleCount