ArrayMetric
-
UML 图
-
结构示意图:
数据采集原理
处理数据的核心数据结构是 LeapArray,采用滑动窗口算法。
LeapArray 中 5 个属性的含义:
- int windowLengthInMs
窗口大小(长度)l - int sampleCount
样本数 n - int intervalInMs
采集周期 t - AtomicReferenceArray<WindowWrap
> array
窗口数组,array 长度就是样本数 n - ReentrantLock updateLock
人如其名,用来更新窗口数据的锁,保证数据的正确性
窗口大小的计算公式:l = t / n,设 t = 1s,n = 5,则 l = 1s / 5 = 200ms,后面若无特殊说明均以该配置来模拟收集统计数据的过程。(Sentinel 默认的样本数是 2,默认采集周期是 1s)
WindowWrap 3 个属性的含义:
- long windowLengthInMs
窗口大小(长度)l,这个与 LeapArray 一致 - long windowStart
窗口开始时间戳,它的值是 l 的整数倍 - T value
这里的泛型 T ,Sentinel 目前只有 MetricBucket 类型,存储统计数据
MetricBucket 2 个属性的含义:
- LongAdder[] counters
counters 的长度是需要统计的事件种类数,目前是 6 个。LongAdder 是线程安全的计数器,性能优于 AtomicLong - volatile long minRt
记录最小的 RT,默认值是 5000ms
LeapArray 统计数据的大致思路:创建一个长度为 n 的数组,数组元素就是窗口,窗口包装了 1 个指标桶,桶中存放了该窗口时间范围中对应的请求统计数据。
可以想象成一个环形数组在时间轴上向右滚动,请求到达时,会命中数组中的一个窗口,那么该请求的数据就会存到命中的这个窗口包含的指标桶中。
当数组转满一圈时,会回到数组的开头,而此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。具体过程如下图:
时间轴坐标为相当时间,以第一次滚动开始时间为 0ms。 下面以图中的 3 个请求来分析数据是如何记录下来的:
-
100ms 时收到第 1 个请求
我们的目的是从数组中找一个合适的窗口来存放统计数据。那么先计算出数组下标 idx = (currentTime / l) % n = (100 / 200) % 5 = 0
同时还要计算出本次请求对应的窗口开始时间:curWindowStart = currentTime - (currentTime % l)= 110 - (100 % 200) = 0ms
现在我们取 window0,因为这是第一次使用 window0,所以先要实例化一下,window0.windowStart 直接取前面计算出的 curWindowStart,即 0ms -
500ms 时收到第 2 个请求
req 落在 400~600ms 之间,同样先计算数组下标 idx = (500 / 200) % 5 = 2,本次请求对应的窗口开始时间:curWindowsStart = 500 - (500 % 200) = 400ms
同样 window2 也是第一次使用,也是先实例化一下,window2.windowStart 也是直接取 curWindowsStart,即 400ms -
1100ms 时收到第 3 个请求
此时环形数组转完了 1 圈,同样先找数组下标 idx = (1100 / 200) % 5 = 0,本次请求对应的窗口开始时间:curWindowsStart = 1100 - (1100 % 200) = 1000ms
对应的就是 window0,由于在第 1 个请求中已经实例化过了,这里就不需要在初始化了。 此时 curWindowsStart(1000ms) > window0.windowStart(0ms),
说明 window0 是一个过期的窗口,需要更新。因为在 1000~1200ms 之间,可能会有多个请求到达,存在并发更新 window0 的情况,那么 updateLock 派上用场了。
更新操作其实就是将 windows0.windowsStart 置为本次的 curWindowsStart,即 1000ms,同时将底层 MetricBucket 中所有计数器的值重置为 0。接下来,记录统计数据就好了。
窗口的变化如下图:
代码实现
LeapArray 获取当前时间窗口的方法:com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow()
/**
* Get the bucket at current timestamp.
*
* @return the bucket at current timestamp
*/
public WindowWrap<T> currentWindow() {
return currentWindow(TimeUtil.currentTimeMillis());// time-tick
}
核心方法:com.alibaba.csp.sentinel.slots.statistic.base.LeapArray#currentWindow(long)
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));// 创建一个窗口,包含一个 bucket
if (array.compareAndSet(idx, null, window)) {// cas 操作,确保只初始化一次
// 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);// 成功则更新,重置窗口开始时间为本次计算出的窗口开始时间,计数器重置为 0
} 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));
}
}
}
取窗口的方法与前面分析的 3 次请求对照起来看,知道怎么取窗口之后,接下来就是存取数据了
ArrayMetric 实现了 Metric 中存取数据的接口方法,选 1 存 1 取两个方法:
- 存数据:
com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#addRT
value 是 MetricBucket 对象,看一下public void addRT(long rt) { WindowWrap<MetricBucket> wrap = data.currentWindow();// 取窗口 wrap.value().addRT(rt); // 计数 }
com.alibaba.csp.sentinel.slots.statistic.data.MetricBucket#addRT
public void addRT(long rt) { add(MetricEvent.RT, rt); // 记录 RT 时间对 rt 值 // Not thread-safe, but it's okay. if (rt < minRt) { // 记录最小响应时间 minRt = rt; } }
public MetricBucket add(MetricEvent event, long n) { counters[event.ordinal()].add(n); // 取枚举顺序对应 counters 数组中的计数器,累加 rt 值 return this; }
- 取数据:
com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#rt
取出 bucket 的方法需要关注一下:public long rt() { data.currentWindow();// 获取当前时间对应的窗口 long rt = 0; List<MetricBucket> list = data.values();// 取出所有的 bucket for (MetricBucket window : list) { rt += window.rt();// 求和 } return rt; }
在获取数据前调用了一次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;// 给定时间(通常是当前时间)与窗口开始时间超过了一个采集周期 }
data.currentWindow()
,在实际取数据的过程中,时间仍在流逝,所以遍历窗口时仍会过滤掉过期的窗口