Sentinel:ArrayMetric

ArrayMetric

  1. UML 图
    UML

  2. 结构示意图:

数据采集原理

处理数据的核心数据结构是 LeapArray,采用滑动窗口算法。

LeapArray 中 5 个属性的含义:

  1. int windowLengthInMs
    窗口大小(长度)l
  2. int sampleCount
    样本数 n
  3. int intervalInMs
    采集周期 t
  4. AtomicReferenceArray<WindowWrap> array
    窗口数组,array 长度就是样本数 n
  5. ReentrantLock updateLock
    人如其名,用来更新窗口数据的锁,保证数据的正确性

窗口大小的计算公式:l = t / n,设 t = 1s,n = 5,则 l = 1s / 5 = 200ms,后面若无特殊说明均以该配置来模拟收集统计数据的过程。(Sentinel 默认的样本数是 2,默认采集周期是 1s)

WindowWrap 3 个属性的含义:

  1. long windowLengthInMs
    窗口大小(长度)l,这个与 LeapArray 一致
  2. long windowStart
    窗口开始时间戳,它的值是 l 的整数倍
  3. T value
    这里的泛型 T ,Sentinel 目前只有 MetricBucket 类型,存储统计数据

MetricBucket 2 个属性的含义:

  1. LongAdder[] counters
    counters 的长度是需要统计的事件种类数,目前是 6 个。LongAdder 是线程安全的计数器,性能优于 AtomicLong
  2. volatile long minRt
    记录最小的 RT,默认值是 5000ms

LeapArray 统计数据的大致思路:创建一个长度为 n 的数组,数组元素就是窗口,窗口包装了 1 个指标桶,桶中存放了该窗口时间范围中对应的请求统计数据。
可以想象成一个环形数组在时间轴上向右滚动,请求到达时,会命中数组中的一个窗口,那么该请求的数据就会存到命中的这个窗口包含的指标桶中。
当数组转满一圈时,会回到数组的开头,而此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。具体过程如下图:

时间轴坐标为相当时间,以第一次滚动开始时间为 0ms。 下面以图中的 3 个请求来分析数据是如何记录下来的:

  1. 100ms 时收到第 1 个请求
    我们的目的是从数组中找一个合适的窗口来存放统计数据。那么先计算出数组下标 idx = (currentTime / l) % n = (100 / 200) % 5 = 0
    同时还要计算出本次请求对应的窗口开始时间:curWindowStart = currentTime - (currentTime % l)= 110 - (100 % 200) = 0ms
    现在我们取 window0,因为这是第一次使用 window0,所以先要实例化一下,window0.windowStart 直接取前面计算出的 curWindowStart,即 0ms

  2. 500ms 时收到第 2 个请求
    req 落在 400~600ms 之间,同样先计算数组下标 idx = (500 / 200) % 5 = 2,本次请求对应的窗口开始时间:curWindowsStart = 500 - (500 % 200) = 400ms
    同样 window2 也是第一次使用,也是先实例化一下,window2.windowStart 也是直接取 curWindowsStart,即 400ms

  3. 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 取两个方法:

  1. 存数据:com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#addRT
    public void addRT(long rt) {
         WindowWrap<MetricBucket> wrap = data.currentWindow();//  取窗口
         wrap.value().addRT(rt); // 计数
     }
    
    value 是 MetricBucket 对象,看一下 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;
     }
    
  2. 取数据:com.alibaba.csp.sentinel.slots.statistic.metric.ArrayMetric#rt
        public long rt() {
         data.currentWindow();// 获取当前时间对应的窗口
         long rt = 0;
         List<MetricBucket> list = data.values();// 取出所有的 bucket
         for (MetricBucket window : list) {
             rt += window.rt();// 求和
         }
         return rt;
     }
    
    取出 bucket 的方法需要关注一下:
    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(),在实际取数据的过程中,时间仍在流逝,所以遍历窗口时仍会过滤掉过期的窗口

参考

posted @ 2020-06-14 15:05  关小曦  阅读(843)  评论(0编辑  收藏  举报