流量数据统计
上面我说了各种流控规则如何进行限流,那这个流量数据是如何统计出来的呢?这里我们回到StatisticNode
//每秒统计数据
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
IntervalProperty.INTERVAL);
/**
* Holds statistics of the recent 60 seconds. The windowLengthInMs is deliberately set to 1000 milliseconds,
* meaning each bucket per second, in this way we can get accurate statistics of each second.
*/
//每分钟统计数据
private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false);
/**
* The counter for thread count.
*/
//线程总数
private AtomicInteger curThreadNum = new AtomicInteger(0);
我们可以看到统计节点主要是保存三种实时统计指标,每秒统计信息、每分钟的统计信息、并发线程数
Metric接口定义了获取各种统计信息的方法,如
long success();
long block();
long exception();
long pass();
long rt();
......
ArrayMetric底层又是通过LeapArray来完成数据的统计,其中几个重要的属性
protected int windowLengthInMs;//每个窗口时间长度
protected int sampleCount;//时间窗口个数
protected int intervalInMs;//总的间隔时间
protected final AtomicReferenceArray<WindowWrap<T>> array;//存放时间窗口的集合
public LeapArray(int sampleCount, int intervalInMs) {
//每个窗口时间长度
this.windowLengthInMs = intervalInMs / sampleCount;
//总的间隔时间
this.intervalInMs = intervalInMs;
//窗口个数
this.sampleCount = sampleCount;
//存放窗口类
this.array = new AtomicReferenceArray<>(sampleCount);
}
这里就是将时间按照窗口数量化分成相同大小的环形桶,当前请求过来时,根据请求的时间查看落在那个时间窗口内,这里我们以成功通过后的数据统计为入口
public WindowWrap<T> currentWindow(long timeMillis) {
//当前时间不能小于0
if (timeMillis < 0) {
return null;
}
//计算当前时间的索引
int idx = calculateTimeIdx(timeMillis);
// Calculate current bucket start time.
//时间窗的起始时间
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)) {//CSA尝试将这个位置的窗口替换
//替换成功,返回新创建的窗口
return window;
} else {
//更新失败,说明有其他线程在竞争,让出执行权限
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 {
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;
// Calculate current index so we can map the timestamp to the leap array.
return (int)(timeId % array.length());//计算当前时间在时间窗口数组中的索引
}
然后根据当前时间计算这个桶对应的起始值
//求出所在窗口的起始时间
protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
return timeMillis - timeMillis % windowLengthInMs;
}
然后根据索引值idx和windowStart计算有几种结果
- 索引idx位置的时间窗口还没有创建,则说明是这个窗口内的第一次请求,则需要新创建一个时间窗
- 索引idx位置的时间窗已经创建
- 如果windowStart > oldwindowStart:说明idx处的时间窗口已经过期是一个无效的时间窗口,则需要重置这个时间窗口windowStart和统计数据为初始值就可以继续使用了
- 如果windowStart < oldwindowStart:不可能发生,除非修改了时间
- 如果windowStart = oldwindowStart:说明我们本次请求的时间刚好和idx位置的时间窗口匹配,则返回这个时间窗
时间窗定义的重要属性
private final long windowLengthInMs;//时间窗口长度
private long windowStart;//窗口起始时间
private T value;//统计数据
那每个窗口内的数据是怎么统计的呢?其实是通过MetricBucket类
private final LongAdder[] counters;
private volatile long minRt;//最小RT时间
public MetricBucket() {
//流控结果
MetricEvent[] events = MetricEvent.values();
//计数器
this.counters = new LongAdder[events.length];
for (MetricEvent event : events) {
counters[event.ordinal()] = new LongAdder();
}
initMinRt();
}
这里统计的维度定义就在MetricEvent
public enum MetricEvent {
/**
* Normal pass.
*/
PASS,
/**
* Normal block.
*/
BLOCK,
EXCEPTION,
SUCCESS,
RT,
/**
* Passed in future quota (pre-occupied, since 1.5.0).
*/
OCCUPIED_PASS
}
获取所有的MetricEvent,然后创建LongAdder计数器数组,并进行初始化同事对minRt进行初始化。最终所有统计数据其实都是通过LongAdder来进行计数,这个类在高并发下有更高的性能。
我们看下如何获取统计数据
@Override
public long success() {
data.currentWindow();
long success = 0;
//获取有效结果的时间窗
List<MetricBucket> list = data.values();
for (MetricBucket window : list) {
success += window.success();
}
return success;
}
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;
}
这里通过当前时间获取有效结果的时间窗集合,然后遍历所有时间窗集合然后或的成功的请求总数。