15. 滑动时间窗口在流控FlowSlot中的使用源码解析
1. 分析入口源码
之前我们通过分析源码已经知道了滑动窗口算法在Sentinel中的应用,那么这节课我们来研究一下流控的FlowSlot是如何使用的
// FlowSlot.java
@Override
public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
boolean prioritized, Object... args) throws Throwable {
//检测并且应用流控规则
checkFlow(resourceWrapper, context, node, count, prioritized);
//触发下一个Slot
fireEntry(context, resourceWrapper, node, count, prioritized, args);
}
void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
throws BlockException {
// 从这里进入
checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
}
这里cheker.checkFlow方法里面找到遍历所有规则的canPassCheck方法然后在进入canPass方法,从而找到DefaultController对应实现,快速失败的流控效果,我们从这里来看,这里我们要关注的是avgUsedTokens方法,这个方法实际上就是获取当前时间窗里面的已经统计的数据。
2. DefaultController快速失败滑动时间窗使用
@Override
public boolean canPass(Node node, int acquireCount, boolean prioritized) {
//获取当前node节点的线程数或者请求通过的qps总数
//获取当前时间窗已经统计的数据
int curCount = avgUsedTokens(node);
//当前请求数(请求的令牌)+申请总数是否(已经消耗的令牌)>该资源配置的总数(阈值)
// 以前的数据+新的数据
if (curCount + acquireCount > count) {
if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
long currentTime;
long waitInMs;
currentTime = TimeUtil.currentTimeMillis();
waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
node.addWaitingRequest(currentTime + waitInMs, acquireCount);
node.addOccupiedPass(acquireCount);
sleep(waitInMs);
// PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
throw new PriorityWaitException(waitInMs);
}
}
return false;
}
return true;
}
所以这里我们跟进一下,看如何获取已经统计的数据
private int avgUsedTokens(Node node) {
// 如果没有选出node,代表没有做统计工作,直接返回0
if (node == null) {
return DEFAULT_AVG_USED_TOKENS;
}
// 判断阈值类型,如果为QPS,则返回当前时间窗统计的QPS
// 如果为线程数,则返回当前时间窗统计的线程数总量
return grade == RuleConstant.FLOW_GRADE_THREAD ? node.curThreadNum() : (int)(node.passQps());
}
3. passQps和curThreadNum方法解析
通过跟踪代码,我们发现最终会进入到StatisticNode.passQps和EntranceNode.curThreadNum方法。如下:
public class StatisticNode implements Node {
// 这里创建了一个一秒的时间窗口, 所以通过他获取到的都是一秒时间窗的数据,默认的样本窗口为2个。
// 这里其实还有一个一分钟的时间窗,用于其他统计
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
...
@Override
public double passQps() {
// rollingCounterInSecond.pass() 当前时间窗中统计的通过请求数量
// rollingCounterInSecond.getWindowIntervalInSec() 时间窗口长度
// 这两个数相除,计算出的就是QPS
return rollingCounterInSecond.pass() / rollingCounterInSecond.getWindowIntervalInSec();
}
...
}
// LeapArray.java
public double getIntervalInSecond() {
//返回当前时间窗的秒数,如果是1分钟返回60秒,如果是1秒中,返回1秒
return intervalInMs / 1000.0;
}
这的rollingCounterInSecond 是Metric 类型的,而Metric是滑动时间窗的接口。具体是ArrayMetric由来实现的。所以通过其方法pass和getWindowIntervalInSec获取到的是滑动窗口的数据,pass为当前窗口的通过的请求数,getWindowIntervalInSec为当前时间窗口跨越的时间秒数。
那么这里就需要查看pass方法,看是如何统计通过请求总量的
// ArrayMetric.java
@Override
public long pass() {
// 更新array中当前时间点所在样本窗口实例中的数据
data.currentWindow();
long pass = 0;
// 将当前时间窗口中的所有样本窗口统计的value读取出来,并且记录
List<MetricBucket> list = data.values();
for (MetricBucket window : list) {
pass += window.pass();
}
return pass;
}
从代码可以看出, 先执行当前时间窗的更新,然后把时间窗所有的样本窗口的通过请求数加起来得到时间窗的请求数。我们跟踪下values方法看看是如何获取到样本窗口数据的
// LeapArray.java
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);
// 这个遍历array中的每一个样本窗口实例
for (int i = 0; i < size; i++) {
WindowWrap<T> windowWrap = array.get(i);
// 若当前遍历实例为空或者已经过时(是否是timeMillis时间所在时间窗内),则继续下一个
if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
continue;
}
result.add(windowWrap.value());
}
//最终会得到1秒或1分钟时间窗口的数据
return result;
}
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
// 当前时间-样本窗口起始时间>时间窗口(按秒就是1秒,按分钟就是一分钟) 说明过时了
return time - windowWrap.windowStart() > intervalInMs;
}
4. 源码结构图
至此,我们Sentenel源码分析全部完成。