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));
            }
        }
    }
posted @ 2021-11-02 10:07  龘人上天  阅读(161)  评论(0编辑  收藏  举报