zoukankan      html  css  js  c++  java
  • Sentinel笔记-滑动窗口

    服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,至少确保服务不会奔溃。

    常见的服务降级实现方式有:

    开关降级、

      人工或者设置定时开关,接口直接返回默认值,适用于促销活动等可以明确预估到并发会突增的场景。

    限流降级、

      假设服务 A 需要依赖服务 B 完成客户端的一次请求,服务 B 处理最大并发请求就是限流,超过最大QPS,B就直接拒绝以保护自己。

      降级方式包括:快速失败,warm up,排队等待

    熔断降级。

      假设服务 A 需要依赖服务 B 完成客户端的一次请求,在服务 B“不行”的时候不再去请求服务 B,这就是熔断,并且B恢复后A需要感知得到,所以熔断需要一个时间周期。

      常见熔断策略:慢调用比例,异常比例,异常数量

    Sentinel与 Hystrix对比

      参照: https://mp.weixin.qq.com/s/12mjY9KawMoyc_DjC883uQ

    Sentinel 是基于滑动窗口实现的实时指标数据统计

    AtomicReferenceArray

    提供了可以原子读取和写入的底层引用数组的操作,并且还包含高级原子操作。如:getAndSet,compareAndSet等。

    滑动窗口具体实现类结构图

    Bucket 记录一段时间内的各项指标数据用的是一个 LongAdder 数组,LongAdder 保证了数据修改的原子性,并且性能比 AtomicInteger 表现更好。数组的每个元素分别记录一个时间窗口内的请求总数、异常数、总耗时。

    Sentinel 用枚举类型 MetricEvent 的 ordinal 属性作为下标,ordinal 的值从 0 开始,按枚举元素的顺序递增,正好可以用作数组的下标。

    public enum MetricEvent {
        PASS,
        BLOCK,
        EXCEPTION,
        SUCCESS,
        RT,
        OCCUPIED_PASS
    }
    public class MetricBucket {
        private final LongAdder[] counters;
        
        //直接使用MetricEvent作为index操作数组
        public MetricBucket add(MetricEvent event, long n) {
            counters[event.ordinal()].add(n);
            return this;
        }
    
        public long pass() {
            return get(MetricEvent.PASS);
        }
        
    }
    

     滑动窗口:

    如果我们希望能够知道某个接口的每秒处理成功请求数(成功 QPS)、每秒处理失败请求数(失败 QPS),以及处理每个成功请求的平均耗时(avg RT),我们只需要控制 Bucket 统计一秒钟的指标数据即可。

    Bucket不可能无限大,当我们只需要保留一分钟的数据时,Bucket 数组的大小就可以设置为 60,我们希望这个数组可以循环使用,并且永远只保存最近 1 分钟的数据,这样不仅可以避免频繁的创建 Bucket,也减少内存资源的占用。但是Bucket不可能无限大,所以使用了

     因为使用循环数组存储数据,涉及到以下问题,定位和判断过期

    定位:

    private int calculateTimeIdx( long timeMillis) {
    
            //当前时间戳一共经历了多少bucket,当前时间戳/bucket时长
            long timeId = timeMillis / windowLengthInMs;
           
            //计算当前时间戳落在数组具体位置
            return (int) (timeId % array.length());
        }
        
        /**
         *  计算当前时间对应的窗口的开始时间,当前时间戳减去(之前完整的bucket数量)
         *	获取bucket开始时间戳, 去掉毫秒部分     
         */
        protected long calculateWindowStart(long timeMillis) {
            /**
             * 假设窗口大小为1000毫秒,即数组每个元素存储1秒钟的统计数据
             * timeMillis % windowLengthInMs 就是取得毫秒部分
             * timeMillis - 毫秒数 = 秒部分
             * 这就得到每秒的开始时间戳
             */
            return timeMillis - timeMillis % windowLengthInMs;
        }

     因为 Bucket 自身并不保存时间窗口信息,所以 Sentinel 给 Bucket 加了一个包装类 WindowWrap,用于记录 Bucket 的时间窗口信息,WindowWrap 源码如下。

    只要知道时间窗口的开始时间和窗口时间大小,只需要给定一个时间戳,就能知道该时间戳是否在 Bucket 的窗口时间内,见方法 isTimeInWindow

    public class WindowWrap<T> {
    
      /** * 窗口时间长度(毫秒) */ 
      private final long windowLengthInMs; 
    
      /** * 开始时间戳(毫秒) */ 
      private long windowStart; 
    
      /** * 统计数据,实际上是类 MetricBucket */ 
      private T value; 
      
      /** 检查给定的时间戳是否在当前 bucket 中。     */
      public boolean isTimeInWindow(long timeMillis) {
            return windowStart <= timeMillis && timeMillis < windowStart + windowLengthInMs;
        }
    }
    
    

    通过时间戳定位 Bucket 当接收到一个请求时,可根据接收到请求的时间戳计算出一个数组索引,从滑动窗口(WindowWrap 数组)中获取一个 WindowWrap,从而获取 WindowWrap 包装的 Bucket,调用 Bucket 的 add 方法记录相应的事件。 核心方法如下:

    /**
         * 根据时间戳获取 bucket
         *
         * @param timeMillis 时间戳(毫秒)
         * @return 如果时间有效,则在提供的时间戳处显示当前存储桶项;如果时间无效,则为空
         */
        public WindowWrap<T> currentWindow(long timeMillis) {
            if (timeMillis < 0) {
                return null;
            }
            // 获取时间戳映射到的数组索引
            int idx = calculateTimeIdx(timeMillis);
            // 计算 bucket 时间窗口的开始时间
            long windowStart = calculateWindowStart(timeMillis);
    
            // 从数组中获取 bucket
            while (true) {
                WindowWrap<T> old = array.get(idx);
                // 一般是项目启动时,时间未到达一个周期,数组还没有存储满,没有到复用阶段,所以数组元素可能为空
                if (old == null) {
                    // 创建新的 bucket,并创建一个 bucket 包装器
                    WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                    // cas 写入,确保线程安全,期望数组下标的元素是空的,否则就不写入,而是复用
                    if (array.compareAndSet(idx, null, window)) {
                        return window;
                    } else {
                        Thread.yield();
                    }
                }
                // 如果 WindowWrap 的 windowStart 正好是当前时间戳计算出的时间窗口的开始时间,则就是我们想要的 bucket
                else if (windowStart == old.windowStart()) {
                    return old;
                }
                // 复用旧的 bucket
                else if (windowStart > old.windowStart()) {
                    if (updateLock.tryLock()) {
                        try {
                            // 重置 bucket,并指定 bucket 的新时间窗口的开始时间
                            return resetWindowTo(old, windowStart);
                        } finally {
                            updateLock.unlock();
                        }
                    } else {
                        Thread.yield();
                    }
                }
                // 计算出来的当前 bucket 时间窗口的开始时间比数组当前存储的 bucket 的时间窗口开始时间还小,
                // 直接返回一个空的 bucket 就行
                else if (windowStart < old.windowStart()) {
                    return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                }
            }
        }
  • 相关阅读:
    C语言程序设计习题参考答案
    C语言程序设计 数据类型转换
    C语言程序设计 练习题参考答案 第二章
    计算机等级考试二级C语言考试复习五要点
    计算机等级考试二级快速复习法
    C语言程序设计 辗转相除法
    ReportViewer (RDLC) 中的换行符是什么
    关于axis2中对soapfault的处理的一个小bug
    java多线程中利用优先级来分配CPU时间的实例
    马云演讲
  • 原文地址:https://www.cnblogs.com/snow-man/p/15497535.html
Copyright © 2011-2022 走看看