StatisticSlot是sentinel责任链中的一个,作用是从多个维度(入口流量、调用者、当前被访问资源)统计响应时间、并发线程数、处理失败个数、处理成功个数等,内部采用了滑动窗口算法。本文将详细介绍滑动窗口算法原理以及StatisticSlot如何实现。
1、滑动窗口算法
滑动窗口算法其实更多的描述的是一种思想,它在内部建立若干个窗口,根据要求将数据落入到不同的窗口中,这样我们可以根据窗口对数据做出统计或计算。根据不同的场景,窗口可以有不同的含义,比如局限于StatisticSlot中,窗口大小表示时间间隔,在一些场景中,窗口大小还可以是字符串长度。
下面介绍一下滑动窗口算法在StatisticSlot中是如何应用的。
StatisticSlot内部建立多个窗口,默认是2个,每个窗口大小都是固定的,窗口大小表示时间间隔。当外部访问资源时,根据访问时间将请求落入对应的窗口内。这样便可以计算最近一段时间总的请求数,QPS等数据。随着窗口的滑动,过期的窗口被剔除,然后创建新的窗口。
下面图片来资源sentinel官网:
图上创建了5个窗口,每个窗口大小是200ms,第一个图的窗口起始时间是200ms,当前时间是910ms,那么此时请求会落入第四个窗口中(800ms~1000ms)。时间走到1001ms时,200ms~400ms的窗口过期,窗口向右滑动,新加入一个1200ms~1400ms的窗口,之后新请求会落入这个新加入的窗口中。
2、StatisticSlot实现原理解析
在StatisticSlot中,使用WindowWrap对象表示窗口。WindowWrap有三个属性:
- windowLengthInMs:窗口大小,单位是毫秒;
- windowStart:窗口的起始时间;
- value:存储时间窗口内的统计数据,比如窗口内的请求数等,该属性使用了泛型。
这里需要重点介绍一下WindowWrap的value属性,该属性使用了泛型,因此可以表示不同类的对象。在StatisticSlot中,value表示的是MetricBucket对象。MetricBucket可以存储6中统计数据,分别是:
- 异常数(EXCEPTION,被访问的资源抛出异常或者通过Tracer记录的异常)
- 成功数(SUCCESS)
- 响应时间(RT)
- 接收请求数(PASS,与SUCCESS的区别是,SUCCESS是资源已经访问完毕,而PASS仅仅是收到请求后允许访问资源但是还未访问,还没有开始访问资源)
- 阻塞请求数(BLOCK,因为不符合流控规则或者服务被降级等原因而禁止访问资源)
- 占用未来的访问请求数(OCCUPIED_PASS,如果超过了FlowRule规定的单位时间内最大请求数或线程数,会被阻止访问资源,但是允许具有优先级的请求占用未来的访问请求数,对于这种请求,sentinel会先占用未来时间窗口的令牌,然后阻塞线程一段时间,之后放行线程访问资源)。
上面每种统计数据都有对应的枚举值,这些值在类MetricEvent中定义。
MetricBucket使用LongAdder的数组记录这些统计数据:
private final LongAdder[] counters;
时间窗口WindowWrap介绍完了,还缺少一个管理WindowWrap对象的类。sentinel使用LeapArray管理这些对象。LeapArray将这些对象组织成一个数组array:
//protected final AtomicReferenceArray<WindowWrap<T>> array;
//sampleCount表示时间窗口的个数
this.array = new AtomicReferenceArray<>(sampleCount);
在LeapArray中提供了获取时间窗口的方法currentWindow():
//入参一般是当前系统时间,也就是线程访问资源的时间,
//currentWindow()方法的作用是根据访问资源的时间获取对应的时间窗口
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
//计算当前时间对应的时间窗口对象在array数组中的下标
int idx = calculateTimeIdx(timeMillis);
//计算时间窗口的起始时间
long windowStart = calculateWindowStart(timeMillis);
while (true) {
//根据下标获取时间窗口对象
WindowWrap<T> old = array.get(idx);
if (old == null) {
//如果时间窗口对象是null,表示该请求是落入时间窗口里面的第一个请求,时间窗口对象还未创建,
//newEmptyBucket()方法用于创建MetricBucket对象
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
//将新建的时间窗口对象设置到array数组中
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield();
}
} else if (windowStart == old.windowStart()) {
//当前请求时间对应的时间窗口在array数组中存在,直接返回
return old;
} else if (windowStart > old.windowStart()) {
//当前请求时间对应的时间窗口起始时间大于array里面的时间,
//意味着array数组里面的时间窗口已经过期,需要将该时间窗口清理掉,换上新的时间窗口
if (updateLock.tryLock()) {
try {
//resetWindowTo()方法由子类实现
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
} else if (windowStart < old.windowStart()) {
//从逻辑上来说,这个分支是不会进入的
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
//计算当前时间对应的时间窗口对象在array数组中的下标
private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
long timeId = timeMillis / windowLengthInMs;
return (int)(timeId % array.length());
}
//计算时间窗口的起始时间
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
return timeMillis - timeMillis % windowLengthInMs;
}
currentWindow()方法返回时间窗口后,sentinel接下来更新MetricBucket里面的统计值或者根据统计值计算QPS。
LeapArray使用了“懒策略”在需要的时候才创建窗口对象,窗口对象过期了也不会实时检查,而是请求过来的时候,才去检查对应的窗口。另外LeapArray的窗口数组是循环使用的。