• 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(),在实际取数据的过程中,时间仍在流逝,所以遍历窗口时仍会过滤掉过期的窗口

    参考

  • 相关阅读:
    675 对象的引用-浅拷贝-深拷贝
    674 vue3侦听器watch
    673 vue计算属性:缓存,setter和getter
    明明有了promise,为啥还需要async await?
    Js常用数组方法汇总
    一些非常有用的Js单行代码
    Js获取验证码倒计时
    前端截取字符串:JS截取字符串之substring、substr和slice详解
    javascript全局变量与局部变量
    JS实现快速排序算法
  • 原文地址:https://www.cnblogs.com/magexi/p/13124870.html
Copyright © 2020-2023  润新知