1 时间窗口基础
参数:
- intervalLength: 区间长度,即统计的时间长度,决定了计算数据时统计的窗口个数
- windowLength:时间窗宽度
- count:时间窗内统计量
- startTime: 每个时间窗的起始时间
如下图所示,每个时间点都会归属于一个时间窗口,即会将时间轴按照时间窗宽度windowLength
进行划分。每个时间窗都有一个起始时间startTime
。
获取某个时间的统计量
- 获取当前时间currTime
- 根据currTime - (已用时间窗).startTime < intervalLength,找到参与统计时间窗
- 对参与时间窗的统计量count进行求和即可得到当前时间的统计量
更新某个时间的统计量
- 获取当前时间currTime
- 找到当前时间所属的时间窗,更新时间窗里面的count
上面的图示是在整个时间轴上进行划分的,有无穷多个时间窗。但是在具体实现上是不可能表示出无穷个时间窗的,所以实现时会使用一个固定大小的时间窗数组。采用复用/循环存储时间窗的方式。依据:在某个时间点只需要统计某几个时间窗的数据即可,而不需要知道所有时间窗的数据,所以只保存需要的几个时间窗
此时多了一个参数sampleCount
,数组的大小就是sampleCount
。
关系:sampleCount
=intervalLength / windowLength
更新某个时间点处的统计量:
- 获取当前时间currTime
- 计算当前时间点所属时间窗在数组中位置index = (currTime / windowLength) % sampleCount
- 获取时间窗window = array[index]
- 判断时间窗是否已过期:currTime - 时间窗.startTime > intervalLength;已过期则重置时间窗口,即将里面的统计量count归零,然后累加count;未过期直接累加count。
获取某个时间点的统计量
- 获取当前时间currTime
- 遍历时间窗数组,判断时间窗是否该统计:currTime - 时间窗.startTime < intervalLength
2 sentinel中时间窗的实现
主要的几个类:
LeapArray
时间窗的底层实现,里面有一个时间窗的数组,数组里面的元素为WindowWrap
,即时间窗WindowWrap<T>
时间窗,T表示要统计的数据,为MetricBucket
MetricBucket
统计量,里面包含了多个具体统计的变量,变量的"类型"由MetrciEvent
决定MetricEvent
统计量类型,和MetricBucktet里面保存的统计变量一一对应ArrayMetric
对外使用的类,隐藏了时间窗的具体实现,其有一个成员变量LeapArray
几个类之间的关系:
2.1 统计的量
需要统计的量在MetricEvent这个枚举变量表示
public enum MetricEvent {
PASS,
BLOCK,
EXCEPTION,
SUCCESS,
RT,
OCCUPIED_PASS
}
2.2 对外提供使用的类
典型用法:
// 创建实例,两个值:2-sampleCount 1000-统计的区间大小
arrayMetric = new ArrayMtric(2, 1000);
// 增加某个统计量的值
arrayMetric.addXXX(n);
// 获取某个统计量的值
arrayMetric.XXX()
当要使用时间窗进行统计时,对外提供的就是ArrayMetric
。它隐藏了具体的时间窗的实现。
它有一个成员变量:data=LeapArray<MetrciBucket>
即底层的时间窗实现类。
在某个时间点需要增加某个统计量的值,就调用addXXX
类型的API
在某个时间点需要获取某个统计量的值,就调用xxx()
类型的API
2.3 底层实现
主要分析两个操作:
- 更新值
- 获取值
首先是时间窗轮转数组LeapArray
,它是整个时间窗组件的主类。
public abstract class LeapArray<T> {
// 时间窗的大小
protected int windowLengthInMs;
// 采样数,即将统计区间划分成几等份
protected int sampleCount;
// 统计区间
protected int intervalInMs;
private double intervalInSecond;
protected final AtomicReferenceArray<WindowWrap<T>> array;
private final ReentrantLock updateLock = new ReentrantLock();
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");
// 1000 / 2 60*1000/60 = 1000
this.windowLengthInMs = intervalInMs / sampleCount;
// 1000 60*1000 60秒
this.intervalInMs = intervalInMs;
// 1 60
this.intervalInSecond = intervalInMs / 1000.0;
// 2 60
this.sampleCount = sampleCount;
// 2
this.array = new AtomicReferenceArray<>(sampleCount);
}
}
2.3.1 更新当前时间点某个统计量
调用ArrayMetric
的addXXX()
方法,这里以addPass()
为例。
- 首先根据当前时间获取所属时间窗,即调用
LeapArray
的currentWindow()
方法。在此方法里面就完成了时间窗的更新,时间窗数组的轮转,具体见currentWindow()
方法。 - 然后调用时间窗的里面的
metricBucket
更新pass
public void addPass(int count) {
// 获取当前时间所属的时间窗
WindowWrap<MetricBucket> wrap = data.currentWindow();
//
wrap.value().addPass(count);
}
LeapArray
的currentWindow()
方法,此方法很重要会更新数组各个位置的时间窗为最新的时间窗,更新即重置时间窗的起始时间以及将里面的统计量进行归零。
public WindowWrap<T> currentWindow() {
return currentWindow(TimeUtil.currentTimeMillis());
}
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 计算当前时间对应的时间窗在数组中的位置:
//
//private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
// long timeId = timeMillis / windowLengthInMs;
// Calculate current index so we can map the timestamp to the leap array.
// return (int)(timeId % array.length());
// }
int idx = calculateTimeIdx(timeMillis);
// Calculate current bucket start time.
// 计算时间窗的起始时间
// protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
// return timeMillis - timeMillis % windowLengthInMs;
// }
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));
// 使用cas方法
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()) {
return old;
// 当前时间所属时间窗的起始时间大于获取到的时间窗的起始时间
// 则更新获取到的时间起始时间,以及重置时间窗里面的统计量的值
} else if (windowStart > old.windowStart()) {
// 加锁
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.3.2 获取某个统计值
调用ArrayMetric的xxx()方法
public long pass() {
// 获取当前的时间窗口,实际上是更新时间窗
data.currentWindow();
long pass = 0;
// 获取需要进行统计的时间窗里面的MericBucket,这里返回的是List<MetricBucket>
// 实际上也可以看成返回的是List<WindowWrap>
List<MetricBucket> list = data.values();
for (MetricBucket window : list) {
// 遍历计量桶,进行累加,计算通过的值
pass += window.pass();
}
return pass;
}
LeapArray
的values()
方法
public List<T> values() {
return values(TimeUtil.currentTimeMillis());
}
public List<T> values(long timeMillis) {
if (timeMillis < 0) {
return new ArrayList<T>();
}
int size = array.length();
// 初始化
List<T> result = new ArrayList<T>(size);
// 遍历时间窗数组,获取每一个元素
for (int i = 0; i < size; i++) {
WindowWrap<T> windowWrap = array.get(i);
// 对应的时间窗为空,或者不属于当前统计的区间
if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
continue;
}
// 在这里真正的加入值
result.add(windowWrap.value());
}
return result;
}
关键点:
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
// 当前时间值减去当前时间窗口的开始值 是否大于统计区间的值
return time - windowWrap.windowStart() > intervalInMs;
}