当前位置:   article > 正文

14. Sentinel滑动时间窗口算法源码解析

14. Sentinel滑动时间窗口算法源码解析

上节课我们分析了Sentinel的滑动时间窗口算法原理,那么这节课我们来研究一下源码中的具体实现

1. Sentinel中使用滑动时间窗的流程图

从图中可以看出Sentinel在各种地方都使用了时间窗提供的数据。 也是依赖Sentinel的StatisticSlot提供数据由时间窗记录下来。

2. 源码入口分析

首先看StatisticSlot.entry方法中node.addPassRequest(count)方法,这里我之前就提到过用到了滑动窗口算法,那我们来具体分析

  1. // StatisticSlot.java
  2. @Override
  3. public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
  4. boolean prioritized, Object... args) throws Throwable {
  5. ...
  6. // 通过滑动窗口添加线程数
  7. node.increaseThreadNum();
  8. // 通过滑动窗口添加请求数
  9. node.addPassRequest(count);
  10. ...
  11. }
  12. //通过以上方法调用进入DefaultNode.addPassRequest方法
  13. // DefaultNode.java
  14. @Override
  15. public void addPassRequest(int count) {
  16. super.addPassRequest(count);
  17. this.clusterNode.addPassRequest(count);
  18. }
  19. //通过以上方法调用进入StatisticNode.addPassRequest方法
  20. // StatisticNode.java
  21. @Override
  22. public void addPassRequest(int count) {
  23. // 按秒滑动统计
  24. rollingCounterInSecond.addPass(count);
  25. // 按分钟滑动统计
  26. rollingCounterInMinute.addPass(count);
  27. }
  28. //通过以上方法调用进入ArrayMetric.addPass方法
  29. // ArrayMetric.java
  30. @Override
  31. public void addPass(int count) {
  32. // 获取当前时间点所在的样本窗口,这里的data对象实际上是LeapArray<MetricBucket>对象
  33. WindowWrap<MetricBucket> wrap = data.currentWindow();
  34. // 将当前请求的计数量添加到当前样本窗口的统计数据中
  35. wrap.value().addPass(count);
  36. }

这里就会进入LeapArray(环形数组)中的currentWindow方法中,这个环形数组,其实就是Sentinel官方提供的原理图中的环形数组WindowLeapArray

3. 环形数组时间窗相关类

  1. // 环形数组
  2. public abstract class LeapArray<T> {
  3. // 样本窗口长度
  4. protected int windowLengthInMs;
  5. // 一个时间窗中包含的时间窗数量
  6. protected int sampleCount;
  7. // 时间窗长度
  8. protected int intervalInMs;
  9. private double intervalInSecond;
  10. // 这个一个数组,元素为WindowWrap样本窗口
  11. // 注意,这里的泛型 T 实际为 MetricBucket 类型
  12. protected final AtomicReferenceArray<WindowWrap<T>> array;
  13. ......
  14. }
  15. // 样本窗口包装类
  16. public WindowWrap(long windowLengthInMs, long windowStart, T value) {
  17. //样本窗口长度
  18. this.windowLengthInMs = windowLengthInMs;
  19. //样本窗口的起始时间戳
  20. this.windowStart = windowStart;
  21. //当前样本窗口的统计数据 其类型为MetricBucket,包含了多维度的数据
  22. this.value = value;
  23. }

当ArrayMetric.addPass方法中调用data.currentWindow();时进入LeapArray滑动数组方法<br />

  1. //LeapArray.java
  2. public WindowWrap<T> currentWindow() {
  3. // 获取当前时间所在的样本窗口
  4. return currentWindow(TimeUtil.currentTimeMillis());
  5. }
  6. // 根据时间获取样本窗口
  7. public WindowWrap<T> currentWindow(long timeMillis) {
  8. if (timeMillis < 0) {
  9. return null;
  10. }
  11. // 计算时间所在的样本窗口id,即在计算数组LeapArray中的索引
  12. int idx = calculateTimeIdx(timeMillis);
  13. // 计算当前样本窗口的开始时间点
  14. long windowStart = calculateWindowStart(timeMillis);
  15. .....
  16. }
  17. private int calculateTimeIdx(long timeMillis) {
  18. // 计算当前时间在那个样本窗口(样本窗口下标),当前时间/样本窗口长度
  19. long timeId = timeMillis / windowLengthInMs;
  20. // 计算具体索引,这个array就是装样本窗口的数组
  21. return (int)(timeId % array.length());
  22. }
  23. //获得timeMillis时间所在窗口的开始时间
  24. protected long calculateWindowStart(long timeMillis) {
  25. //当前时间 减去 当前时间除以样本窗口的长度的的余数
  26. return timeMillis - timeMillis % windowLengthInMs;
  27. }

timeId(样本窗口下标)原理如下:

正在上传…重新上传取消正在上传…重新上传取消

在环形数组中的下标计算原理图:

4. 样本窗口获取流程分析

当我们拿到样本窗口在环形数组的下标和样本窗口的开始时间后,是如何获得或创建样本窗口的呢? 接下来我们分析这一部分。

通过以下代码可以看出,这里开启了一个死循环,直到获取到样本窗口才退出循环,因为多线程操作,可能其他线程也才操作这个环形数组。 所以这个死循环是必要的。在这个死循环中,会产生以下几种情况

  1. 在环形数组中找不到样本窗口

    这种情况一般发生在程序刚刚运行的时候,需要创建一个样本窗口放入环形数组对应的下标中

  2. 找到样本窗口并且样本窗口开始时间和需要的样本开始时间一致

    则说明要找的样本窗口就是当前数组中索引对应的样本窗口,可返回直接使用。

  3. 找到样本窗口,但需要的样本窗口开始时间大于找到的样本窗口开始时间

    这种情况其实是最常见的, 说明找到的样本窗口过时了,需要对样本窗口reset后重新使用(这里避免了重新创建各种对象,会变得更高效,这也是环形数组的特性)

  4. 找到样本窗口,但需要的样本窗口开始时间小于找到的样本窗口开始时间

    首先基本不会出现这种情况,因为时间不会倒流,除非人为系统调快了时间。 所以这里的逻辑是一个兜底的方法,这里采用了直接创建一个游离在环形数组外的样本窗口返回

  1. while (true) {
  2. // 获取到当前时间所在的样本窗口
  3. WindowWrap<T> old = array.get(idx);
  4. // 如果获取不到,表示没有创建
  5. if (old == null) {
  6. // 创建新的时间窗口
  7. WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
  8. // 通过CAS方式将新建窗口放入Array
  9. if (array.compareAndSet(idx, null, window)) {
  10. return window;
  11. } else {
  12. Thread.yield();
  13. }
  14. } else if (windowStart == old.windowStart()) {
  15. // 若当前样本窗口的起始时间点与计算出的样本窗口起始点相同,则说明两个是同一个样本窗口
  16. return old;
  17. } else if (windowStart > old.windowStart()) {
  18. // 若当前样本窗口的起始时间点 大于 计算出的样本窗口起始时间点,说明计算出的样本窗口已经过时了,
  19. // 需要将原来的样本窗口替换
  20. if (updateLock.tryLock()) {
  21. try {
  22. // 替换掉老的样本窗口
  23. return resetWindowTo(old, windowStart);
  24. } finally {
  25. updateLock.unlock();
  26. }
  27. } else {
  28. Thread.yield();
  29. }
  30. } else if (windowStart < old.windowStart()) {
  31. // 当前样本窗口的起始时间点 小于 计算出的样本窗口起始时间点,
  32. // 这种情况一般不会出现,因为时间不会倒流。除非人为修改了系统时钟
  33. return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
  34. }
  35. }

5. 样本窗口获取流程图

6. 样本窗口重置

在上面获取样本窗口逻辑中,其中第3点中,有一个重置样本窗口逻辑,我们看看它是如何实现的呢

  1. // BucketLeapArray.java
  2. @Override
  3. protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long startTime) {
  4. // 更新样本窗口起始时间
  5. w.resetTo(startTime);
  6. // 将多维度统计数据清零
  7. w.value().reset();
  8. return w;
  9. }
  10. // MetricBucket.java
  11. private final LongAdder[] counters;
  12. // 更新数据分析
  13. public MetricBucket reset() {
  14. // 将每个维度的统计数据清零
  15. for (MetricEvent event : MetricEvent.values()) {
  16. counters[event.ordinal()].reset();
  17. }
  18. initMinRt();
  19. return this;
  20. }

其实就是将多维度的数据清零,然后修改样本窗口的开始时间就可以了。

7. 统计滑动窗口数据维度

最后我们再来看一下具体是那个维度,其实是通过维度

  1. // MetricBucket.java
  2. public void addPass(int n) {
  3. add(MetricEvent.PASS, n);
  4. }
  5. // MetricEvent.java
  6. public enum MetricEvent {
  7. // 通过数维度
  8. PASS,
  9. // 流控数维度
  10. BLOCK,
  11. // 异常数维度
  12. EXCEPTION,
  13. //成功的请求数,在StatisticSlot.exit方法中调用
  14. SUCCESS,
  15. //响应总时间毫秒数,在StatisticSlot.exit方法中调用
  16. RT,
  17. /**
  18. * Passed in future quota (pre-occupied, since 1.5.0).
  19. */
  20. OCCUPIED_PASS
  21. }
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/453759
推荐阅读
相关标签
  

闽ICP备14008679号