滑动时间窗算法
时间窗限流算法
算法原理
系统自动选定一个时间窗口的起始零点,然后按照固定长度将时间轴划分为若干固定长度的时间窗口,所以该窗口也称为“固定时间窗接口”。
当请求到达时,系统会查看该请求到达的时间点所在的时间窗口当前统计的数据是否超出阈值。未超出,则请求通过,否则被限流。
存在的问题
连续两个时间窗口中的统计数据都没有超出阈值,但在跨窗口范围内的统计数据却超出了阈值。
滑动时间窗限流算法
算法原理
滑动时间窗限流算法解决了固定时间窗限流算法的问题。其没有划分固定的时间窗起点与终点,而是将每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。这种时间窗称为“滑动时间窗”。
存在的问题
每次计算一个到达的请求都会进行滑动时间窗的请求量统计,这会带来很多的重复性的计算。
算法改进
采取一种“折中”的改进措施:将整个时间轴拆分为若干“样本窗口”,样本窗口的长度是小于滑动时间窗口长度的。当等于滑动时间窗口长度时,就变为了“固定时间窗口算法”。一版时间窗口长度会是样本窗口长度的整数倍。
那么是如何判断一个请求是否能够通过呢?当到达样本窗口终点时间时,每个样本窗口会统计一次本样本窗口中的流量数据并记录下来。当一个请求到达时,会统计出当前请求时间点所在样本窗口中的流量数据,然后再获取到当前请求时间点所在时间窗中其它样本窗口的统计数据,求和后,如果没有超出阈值,则通过,否则被限流。
源码分析
ProcessorSlotChain(核心骨架):将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起。slot chain 其实可以分为两部分:统计数据构建部分(statistic)和判断部分(rule checking)。 系统会为每个资源创建一套SlotChain。
- 最简单的实现代码如下
//配置规则 initFlowRules(); while (true) { //1.5.0可以直接利用try-with-resource特性 try (Entry entry = SphU.entry("hello world")) { //被保护的逻辑 System.out.println("hello world"); } catch (Exception e) { //处理被流控的逻辑 System.out.println("blocked"); } }
- 代码的执行链路是如何流转的?
滑动时间窗口
Sentinel底层使用LeapArray来实现统计实时的秒级指标数据,比较好的支持写大于读的场景。
//窗口时间间隔,用于统计时间段/窗口数量 protected int windowLengthInMs; //窗口数量 protected int sampleCount; //统计时间段,需为窗口数量的整数倍 protected int intervalInMs; //窗口包装对象队列,只保存sampleCount窗口 protected final AtomicReferenceArray<WindowWrap<T>> array;
LeapArray的主体实现:
private int calculateTimeIdx(/*@Valid*/ long timeMillis) { //timeMillis当前时间戳 long timeId = timeMillis / windowLengthInMs; //计算当前时间属于哪个窗口 return (int)(timeId % array.length()); } protected long calculateWindowStart(/*@Valid*/ long timeMillis) { //计算窗口开始位置 return timeMillis - timeMillis % windowLengthInMs; } //时间在变化,窗口不变 public WindowWrap<T> currentWindow(long timeMillis) { if (timeMillis < 0) { return null; } //当前窗口位置 int idx = calculateTimeIdx(timeMillis); //窗口开始时间 long windowStart = calculateWindowStart(timeMillis); while (true) { WindowWrap<T> old = array.get(idx); if (old == null) { //当前窗口还没有初始化 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 { //让出当前CPU的时间片 Thread.yield(); } } else if (windowStart == old.windowStart()) { //当前时间窗仍有效,直接返回旧窗口 return old; } else if (windowStart > old.windowStart()) { //当前时间所在窗口失效,重置窗口 //保证当前只有一个线程处理,如果已经有线程处理,则让出时间片 if (updateLock.tryLock()) { try { //成功获取到之后释放锁 return resetWindowTo(old, windowStart); } finally { updateLock.unlock(); } } else { //让出当前CPU时间片 Thread.yield(); } } else if (windowStart < old.windowStart()) { //异常情况处理 return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } }
主要流程:
- 计算所给时间所在窗口索引号:idx
- 计算所给时间所在窗口开始时间:windowStart
- 根据索引号idx获取现有窗口对象:window
- 根据现有窗口对象:window,判断是否需要更新当前窗口