Sentinel源码分析二 之 流控算法-滑动时间窗口

记录Sentinel中使用的滑动时间窗口之前,先说明下简单的滑动时间窗口是怎样的。

对于限流的算法假如时间窗口不滑动,限流qps 100。如果在0.5~1s之间发生了80个请求量,会认为在0~1s内qps是不会达到限流阈值的。在1~1.5s内达到80的请求量,也会认为在1-2s内的qps为80没有达到阈值。

Sentinel源码分析二 之 流控算法-滑动时间窗口_第1张图片

但是在0.5~1.5秒的请求量却达到160已经超过了阈值。这就出现了问题。

 

Sentinel源码分析二 之 流控算法-滑动时间窗口_第2张图片

 有了滑动时间时间窗口之后的统计变成下面的样子。把1s分成四个bucket,每个是250ms间隔。

 

假如750ms~1s之间,来了一个请求,统计当前bucket和前面三个bucket中的请求量总和101,大于阈值,就会把当前这个请求进行限流。

假如1s~1.25s之间,来了一个请求,统计当前bucket和前面三个bucket中的请求量总和21,小于阈值,就会正常放行。这里请求总量统计去掉了0~250ms之间的bucket,就是体现了时间窗口的滑动。

但是这里需要注意的一点,时间滑动时间窗口的统计也不是百分百精准的,比如上图中在绿色箭头的地方来了一个请求之后,统计的bucket是250ms 到绿色箭头之间的bucket,但是这个时间区间并没有1s,

因为去掉了0~250ms的bucket统计,所以可能出现偏差,当然这个bucket区间细分的越小就越精准。


 下面分析Sentinel中怎么利用滑动时间窗口进行流控的。

 Metric :是一个记录保护资源的调用指标的基础结构。里面规定了被保护的资源有那些调用指标需要被统计。看一些关键指标。

源码:

public interface Metric extends DebugSupport {

    /**
     * Get total success count.
     * 统计所有成功的次数
     * @return success count
     */
    long success();

    /**
     * Get max success count.
     *
     * @return max success count
     */
    long maxSuccess();

    /**
     * Get total exception count.
     * 统计所有异常的次数
     * @return exception count
     */
    long exception();

    /**
     * Get total block count.
     *
     * @return block count
     */
    long block();

    /**
     * Get total pass count. not include {@link #occupiedPass()}
     * 统计请求通过的次数  不包括 occupiedPass  限流就是使用这个统计的
     * @return pass count
     */
    long pass();

 ArrayMetric 是上面接口的一个实现类。它维护了一个局部变量 LeapArray data 来具体计算被保护资源的调用指标.

部分源码如下:

public class ArrayMetric implements Metric {
    // 滑动时间窗口算法的实现 
    private final LeapArray data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
        // 使用的这个具体类,sampleCount :时间窗口的bucket数量  默认  2个,  intervalInMs: 时间窗口的时间间隔  默认 1000ms  也就是每个bucket是500ms
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    @Override
    public long pass() {
        // 更新最新的时间窗口
        data.currentWindow();
        long pass = 0;
        // 得到所有的统计窗口
        List list = data.values();
        // 每个窗口中的统计量累加起来
        for (MetricBucket window : list) {
            pass += window.pass();
        }
        return pass;
    }
   

    @Override
    public void addPass(int count) {
        // 向当前时间窗口中增加一个请求数量  这个方法会在StatisticSlot 统计qps的时候使用到
       // 时间窗口每个bucket都被WindowWrap包装了下,而且一个MetricBucket 里面可以统计好多维度的数据,使用MetricEvent区分的。
        WindowWrap wrap = data.currentWindow();
        wrap.value().addPass(count);
    }

    public void add(MetricEvent event, long count) {
        data.currentWindow().value().add(event, count);
    }

    @Override
    public double getWindowIntervalInSec() {
        return data.getIntervalInSecond();
    }

    @Override
    public int getSampleCount() {
        return data.getSampleCount();
    }

}

LeapArray: 

public abstract class LeapArray {
    
    // 滑动时间窗口每个bucket的时间长度
    protected int windowLengthInMs;
    // 滑动时间窗口 一共有多少个bucket
    protected int sampleCount;
   // 滑动时间窗口 总的时间窗口  单位 毫秒
    protected int intervalInMs;
   // 滑动时间窗口  总的时间窗口  单位 秒   
    private double intervalInSecond;

   // 每个时间窗口bucket 的存储实例WindowWrap   array 就相当于是整个滑动时间窗口
    protected final AtomicReferenceArray> array;

    /**
     * The conditional (predicate) update lock is used only when current bucket is deprecated.
       更新滑动时间窗口的时候使用
     */
    private final ReentrantLock updateLock = new ReentrantLock();

    /**
     * The total bucket count is: {@code sampleCount = intervalInMs / windowLengthInMs}.
     *
     * @param sampleCount  bucket count of the sliding window
     * @param intervalInMs the total time interval of this {@link LeapArray} in milliseconds
     */
    public LeapArray(int sampleCount, int intervalInMs) {
        AssertUtil.isTrue(sampleCount > 0, "bucket count is invalid: " + sampleCount);
        AssertUtil.isTrue(intervalInMs > 0, "total time interval of the sliding window should be positive");
        AssertUtil.isTrue(intervalInMs % sampleCount == 0, "time span needs to be evenly divided");

        this.windowLengthInMs = intervalInMs / sampleCount;
        this.intervalInMs = intervalInMs;
        this.intervalInSecond = intervalInMs / 1000.0;
        this.sampleCount = sampleCount;

        this.array = new AtomicReferenceArray<>(sampleCount);
    }

    /**
     * Get the bucket at current timestamp.
     * 计算当前时间的bucket实例  每次请求过来都会计算处于那个bucket位置
     * @return the bucket at current timestamp
     */
    public WindowWrap currentWindow() {
      
        return currentWindow(TimeUtil.currentTimeMillis());
    }

    /**
     * Create a new statistic value for bucket.
     *
     * @param timeMillis current time in milliseconds
     * @return the new empty bucket
     */
    public abstract T newEmptyBucket(long timeMillis);

    /**
     * Reset given bucket to provided start time and reset the value.
     *
     * @param startTime  the start time of the bucket in milliseconds
     * @param windowWrap current bucket
     * @return new clean bucket at given start time
     */
    protected abstract WindowWrap resetWindowTo(WindowWrap windowWrap, long startTime);

   // 计算当前时间处于滑动时间窗口数组中的索引位置
    private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
        long timeId = timeMillis / windowLengthInMs;
        // Calculate current index so we can map the timestamp to the leap array.
        return (int)(timeId % array.length());
    }
    // 计算时间窗口bucket的起始时间
    protected long calculateWindowStart(/*@Valid*/ long timeMillis) {
        return timeMillis - timeMillis % windowLengthInMs;
    }

    /**
     * Get bucket item at provided timestamp.
     * 
     * @param timeMillis a valid timestamp in milliseconds
     * @return current bucket item at provided timestamp if the time is valid; null if time is invalid
     */
    public WindowWrap currentWindow(long timeMillis) {
        if (timeMillis < 0) {
            return null;
        }
        // 计算当前时间 在滑动时间窗口array中的索引位置
        int idx = calculateTimeIdx(timeMillis);
        // Calculate current bucket start time.
        // 计算当前时间在时间窗口bucket中的开始时间

        long windowStart = calculateWindowStart(timeMillis);

        /*
         * Get bucket item at given time from the array.
         *
         * (1) Bucket is absent, then just create a new bucket and CAS update to circular array.
         * (2) Bucket is up-to-date, then just return the bucket.
         * (3) Bucket is deprecated, then reset current bucket and clean all deprecated buckets.
         */
        while (true) {
            // 根据当前时间计算的bucket 索引值 在array的数据
            WindowWrap old = array.get(idx);
            if (old == null) {
                /*
                 *     B0       B1      B2    NULL      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            bucket is empty, so create new and update
                 *
                 * If the old bucket is absent, then we create a new bucket at {@code windowStart},
                 * then try to update circular array via a CAS operation. Only one thread can
                 * succeed to update, while other threads yield its time slice.
                   这个图解就很好理解了,比如当前时间计算的bucket 所在的位置在上面的800~1000之间的时候,array是空的,就新建一个时间窗口bucket  WindowWrap
                   通过cas更新到array中 如果cas失败了就让出时间片
                 */
                WindowWrap window = new WindowWrap(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
                if (array.compareAndSet(idx, null, window)) {
                    // 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()) {
                
                /*
                 *     B0       B1      B2     B3      B4
                 * ||_______|_______|_______|_______|_______||___
                 * 200     400     600     800     1000    1200  timestamp
                 *                             ^
                 *                          time=888
                 *            startTime of Bucket 3: 800, so it's up-to-date
                 *
                 * If current {@code windowStart} is equal to the start timestamp of old bucket,
                 * that means the time is within the bucket, so directly return the bucket.
                   如果当前时间计算出来的索引位置已经有了WindowWrap bucket 而且存在的bucket的开始时间和当前计算的开始相等,就返回已经存在的这个WindowWrap
                   在StatisticSlot增加请求数量的时候就会使用这个bucket 中的请求数量进行累加
                 */
                return old;
            } else if (windowStart > old.windowStart()) {
                /*
                 *   (old)
                 *             B0       B1      B2    NULL      B4
                 * |_______||_______|_______|_______|_______|_______||___
                 * ...    1200     1400    1600    1800    2000    2200  timestamp
                 *                              ^
                 *                           time=1676
                 *          startTime of Bucket 2: 400, deprecated, should be reset
                 *
                 * If the start timestamp of old bucket is behind provided time, that means
                 * the bucket is deprecated. We have to reset the bucket to current {@code windowStart}.
                 * Note that the reset and clean-up operations are hard to be atomic,
                 * so we need a update lock to guarantee the correctness of bucket update.
                 *
                 * The update lock is conditional (tiny scope) and will take effect only when
                 * bucket is deprecated, so in most cases it won't lead to performance loss.
                   这个时候就表明 时间窗口要向前滑动了  就是把存在的时间窗口内容进行重置  重置包括开始时间更新  窗口内的计数清零  
                   使用加锁操作
                 */
                if (updateLock.tryLock()) {
                    try {
                        // Successfully get the update lock, now we reset the bucket.
                         // 具体逻辑在的子类  OccuiableBucketLeapArray中

                        return resetWindowTo(old, windowStart);
                    } 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(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
            }
        }
    }

 public List values() {
        return values(TimeUtil.currentTimeMillis());
    }

    public List values(long timeMillis) {
        // 把当前时间窗口中的bucket WindowWrap 都返回出去  用来统计时间窗口总的请求数量
        if (timeMillis < 0) {
            return new ArrayList();
        }
        int size = array.length();
        List result = new ArrayList(size);

        for (int i = 0; i < size; i++) {
            WindowWrap windowWrap = array.get(i);
            if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
                continue;
            }
            result.add(windowWrap.value());
        }
        return result;
    }

 上面滑动时间窗口用了WindowWrap 来表示bucket,但是这个类里又包了一个泛型,使用泛型的目的是为了让这个bucket 可以统计更多类型的数据。比如上面的泛型是MetricBucket。

 WindowWrap :

Sentinel源码分析二 之 流控算法-滑动时间窗口_第3张图片

 MetricBucket:

public class MetricBucket {

    private final LongAdder[] counters;

    private volatile long minRt;

    public MetricBucket() {
        MetricEvent[] events = MetricEvent.values();
        this.counters = new LongAdder[events.length];
        for (MetricEvent event : events) {
            counters[event.ordinal()] = new LongAdder();
        }
        initMinRt();
    }

    public MetricBucket reset(MetricBucket bucket) {
        for (MetricEvent event : MetricEvent.values()) {
            counters[event.ordinal()].reset();
            counters[event.ordinal()].add(bucket.get(event));
        }
        initMinRt();
        return this;
    }

    private void initMinRt() {
        this.minRt = SentinelConfig.statisticMaxRt();
    }

    /**
     * Reset the adders.
     *
     * @return new metric bucket in initial state
     */
    public MetricBucket reset() {
        for (MetricEvent event : MetricEvent.values()) {
            counters[event.ordinal()].reset();
        }
        initMinRt();
        return this;
    }

    public long get(MetricEvent event) {
        return counters[event.ordinal()].sum();
    }

    public MetricBucket add(MetricEvent event, long n) {
        counters[event.ordinal()].add(n);
        return this;
    }

    public long pass() {
        return get(MetricEvent.PASS);
    }

    public long occupiedPass() {
        return get(MetricEvent.OCCUPIED_PASS);
    }

    public long block() {
        return get(MetricEvent.BLOCK);
    }

    public long exception() {
        return get(MetricEvent.EXCEPTION);
    }

    public long rt() {
        return get(MetricEvent.RT);
    }

    public long minRt() {
        return minRt;
    }

    public long success() {
        return get(MetricEvent.SUCCESS);
    }

    public void addPass(int n) {
        add(MetricEvent.PASS, n);
    }

    public void addOccupiedPass(int n) {
        add(MetricEvent.OCCUPIED_PASS, n);
    }

    public void addException(int n) {
        add(MetricEvent.EXCEPTION, n);
    }

    public void addBlock(int n) {
        add(MetricEvent.BLOCK, n);
    }

    public void addSuccess(int n) {
        add(MetricEvent.SUCCESS, n);
    }

    public void addRT(long rt) {
        add(MetricEvent.RT, rt);

        // Not thread-safe, but it's okay.
        if (rt < minRt) {
            minRt = rt;
        }
    }

    @Override
    public String toString() {
        return "p: " + pass() + ", b: " + block() + ", w: " + occupiedPass();
    }
}

统计的维度枚举类:MetricEvent

public enum MetricEvent {

    /**
     * Normal pass.
     */
    PASS,
    /**
     * Normal block.
     */
    BLOCK,
    EXCEPTION,
    SUCCESS,
    RT,

    /**
     * Passed in future quota (pre-occupied, since 1.5.0).
     */
    OCCUPIED_PASS
}

 Sentinel中 qps的统计和限流控制主要是在StatisticSlot,FlowSlot里面完成的。

 先执行StatisticSlot的entry.   fireEntry就是执行下一个slot的。从这里看出,在正向执行的时候它并没有处理什么内容。但是在下面责任链上的entry方法执行完后。

调用了node.increaseThreadNum(); node.addPassRequest(count);  这里就是统计请求通过的线程数量和请求数量。

Sentinel源码分析二 之 流控算法-滑动时间窗口_第4张图片

 我们这里跟踪下node.addPassRequest(count);  用到了时间滑动窗口。node是DefaultNode实例。

DefaultNode#addPassRequest(count)

@Override
    public void addPassRequest(int count) {
        //调用父类 StatisticNode
        super.addPassRequest(count);
        // 集群模式下使用的
        this.clusterNode.addPassRequest(count);
    }

 这里看单机限流模式,跟随StatisticNode#addPassRequest(count);

 @Override
    public void addPassRequest(int count) {
        // 秒级别的滑动时间窗口   看这个就可以了
        rollingCounterInSecond.addPass(count);
        // 分钟级别的滑动时间窗口 
        rollingCounterInMinute.addPass(count);
    }

秒级别的滑动时间窗口:

// 两个参数分别是:2   1000  这里就是构建了一个时间滑动窗口长度 1s,分了两个bucket,一个bucket长度是500ms
private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT,
        IntervalProperty.INTERVAL);

ArrayMetric的部分源码上面有贴出来。 

rollingCounterInSecond.addPass(count);  这个是调用下面这端逻辑。

  @Override
    public void addPass(int count) {
          // 首先获取当前时间的时间窗口,然后向窗口中增加请求量。
        // 向当前时间窗口中增加一个请求数量  这个方法会在StatisticSlot 统计qps的时候使用到
       // 时间窗口每个bucket都被WindowWrap包装了下,而且一个MetricBucket 里面可以统计好多维度的数据,使用MetricEvent区分的。
        WindowWrap wrap = data.currentWindow();
        wrap.value().addPass(count);
    }

data.currentWindow()会调用到LeapArray#currentWindow(long timeMills);的方法上面源码也有分析。

得到窗口之后  wrap.value().addPass(count);  会调用MetricBucket#appPass(int n); 方法

public void addPass(int n) {
        // 时间窗口bucket 中也是有一个  LongAdder[] counter来存储计数统计的,因为可以统计不同维度的数据,比如这里就是统计PASS的数量
        add(MetricEvent.PASS, n);
    }

至于为什么使用LongAdder,因为它的效率比Atomic类的性能更好些。至于为什么就不说了这里。

 public MetricBucket add(MetricEvent event, long n) {
         // event.ordinal() 就是获取枚举值在枚举类中的位置,就是索引值  这个操作是cas的 性能更好些。
        counters[event.ordinal()].add(n);
        return this;
    }

上面分析的就是StatisticSlot 统计qps的过程。

下面看下FlowSlot中怎么做流控限制的。

FlowSlot#entry中

@Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 根据配置的限流规则进行校验
        checkFlow(resourceWrapper, context, node, count, prioritized);

        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

FlowSlot#checkFlow

void checkFlow(ResourceWrapper resource, Context context, DefaultNode node, int count, boolean prioritized)
        throws BlockException {
         //  checker 是 FlowRuleChecker
        checker.checkFlow(ruleProvider, resource, context, node, count, prioritized);
    }

FlowRuleChecker#checkFlow

 public void checkFlow(Function> ruleProvider, ResourceWrapper resource,
                          Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
        if (ruleProvider == null || resource == null) {
            return;
        }
        Collection rules = ruleProvider.apply(resource.getName());
        if (rules != null) {
            for (FlowRule rule : rules) {
                // 开始调用配置的限流规则
if (!canPassCheck(rule, context, node, count, prioritized)) {
                    throw new FlowException(rule.getLimitApp(), rule);
                }
            }
        }
    }


private static boolean passLocalCheck(FlowRule rule, Context context, DefaultNode node, int acquireCount,
                                          boolean prioritized) {
        Node selectedNode = selectNodeByRequesterAndStrategy(rule, context, node);
        if (selectedNode == null) {
            return true;
        }
         // 不同的流控效果,对应不同的rater 这里主要看快速失败DefaultController
        return rule.getRater().canPass(selectedNode, acquireCount, prioritized);
    }

流控效果分为下面三种。分别对应:DefaultController,WarmUpController,RateLimiterController

 DefaultController@canPass

public boolean canPass(Node node, int acquireCount, boolean prioritized) {
        //  计算时间窗口内已经通过的请求数量
        int curCount = avgUsedTokens(node);
        // acquireCount一般就是 1 ,count就是配置的单机阈值  如果条件满足就是达到阈值 直接返回false
        if (curCount + acquireCount > count) {
              //  prioritized 默认是false
            if (prioritized && grade == RuleConstant.FLOW_GRADE_QPS) {
                long currentTime;
                long waitInMs;
                currentTime = TimeUtil.currentTimeMillis();
                waitInMs = node.tryOccupyNext(currentTime, acquireCount, count);
                if (waitInMs < OccupyTimeoutProperty.getOccupyTimeout()) {
                    node.addWaitingRequest(currentTime + waitInMs, acquireCount);
                    node.addOccupiedPass(acquireCount);
                    sleep(waitInMs);

                    // PriorityWaitException indicates that the request will pass after waiting for {@link @waitInMs}.
                    throw new PriorityWaitException(waitInMs);
                }
            }
            return false;
        }
        return true;
    }

如果通过,说明没有达到阈值,然后走到StatisticSlot逻辑的时候进行addPass。这样整个流程就串起来了。

  

你可能感兴趣的:(微服务,算法,java,sentinel)