前言

Sentinel的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等。上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot)StatisticSlotSentinel的整个体系中扮演了一个非常重要的角色,后续的一系列操作(限流,熔断)等都依赖于StatisticSlot所统计出的数据。

本文所要讨论的重点就是StatisticSlot是如何做的流量统计?

其实在之前介绍常用限流算法[常用限流算法](https://www.jianshu.com/p/9edebaa446d3)的时候已经有提到过一个算法滑动窗口限流,该算法的滑动窗口原理其实跟Sentinel所提供的流量统计原理是一样的,都是基于时间窗口的滑动统计

回到StatisticSlot


public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count, boolean prioritized, Object... args) throws Throwable { ... // 当前请求线程数加一 node.increaseThreadNum(); // 新增请求数 node.addPassRequest(count); ... }

可以看到StatisticSlot主要统计了两种类型的数据

  1. 线程数

  2. 请求数(QPS)

对于线程数的统计比较简单,通过内部维护一个LongAdder来进行当前线程数的统计,每进入一个请求加1,每释放一个请求减1,从而得到当前的线程数

对于请求数QPS的统计则相对比较复杂,其中有用到滑动窗口原理(也是本文的重点),下面根据源码来深入的分析

DefaultNode和StatisticNode


public void addPassRequest(int count) { // 调用父类(StatisticNode)来进行统计 super.addPassRequest(count); // 根据clusterNode 汇总统计(背后也是调用父类StatisticNode) this.clusterNode.addPassRequest(count); }

最终都是调用了父类StatisticNodeaddPassRequest方法


/** * 按秒统计,分成两个窗口,每个窗口500ms,用来统计QPS  */ private transient volatile Metric rollingCounterInSecond = new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL); /** * 按分钟统计,分成60个窗口,每个窗口 1000ms  */ private transient Metric rollingCounterInMinute = new ArrayMetric(60, 60 * 1000, false); public void addPassRequest(int count) { rollingCounterInSecond.addPass(count); rollingCounterInMinute.addPass(count); }

代码比较简单,可以知道内部是调用了ArrayMetricaddPass方法来统计的,并且统计了两种不同时间维度的数据(秒级和分钟级)

ArrayMetric


private final LeapArray<MetricBucket> data; public ArrayMetric(int sampleCount, int intervalInMs) { this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs); } public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) { if (enableOccupy) { this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs); } else { this.data = new BucketLeapArray(sampleCount, intervalInMs); } } public void addPass(int count) { // 1\. 获取当前窗口 WindowWrap<MetricBucket> wrap = data.currentWindow(); // 2\. 当前窗口加1 wrap.value().addPass(count); }

ArrayMetric其实也是一个包装类,内部通过实例化LeapArray的对应实现类,来实现具体的统计逻辑,LeapArray是一个抽象类,OccupiableBucketLeapArrayBucketLeapArray都是其具体的实现类

OccupiableBucketLeapArray在1.5版本之后才被引入,主要是为了解决一些高优先级的请求在限流触发的时候也能通过(通过占用未来时间窗口的名额来实现) 也是默认使用的LeapArray实现类

而统计的逻辑也比较清楚,分成了两步:

  1. 定位到当前窗口

  2. 获取到当前窗口WindowWrapMetricBucket并执行addPass逻辑

这里我们先看下第二步中的MetricBucket类,看看它做了哪些事情

MetricBucket


/**  * 存放当前窗口各种类型的统计值(类型包括 PASS BLOCK EXCEPTION 等)  */ private final LongAdder[] counters; public MetricBucket() { MetricEvent[] events = MetricEvent.values(); this.counters = new LongAdder[events.length]; for (MetricEvent event : events) { counters[event.ordinal()] = new LongAdder(); } initMinRt(); } // 统计pass数 public void addPass(int n) { add(MetricEvent.PASS, n); } // 统计可占用的pass数 public void addOccupiedPass(int n) { add(MetricEvent.OCCUPIED_PASS, n); } // 统计异常数 public void addException(int n) { add(MetricEvent.EXCEPTION, n); } // 统计block数 public void addBlock(int n) { add(MetricEvent.BLOCK, n); } ....

MetricBucket通过定义了一个LongAdder数组来存储不同类型的流量统计值,具体的类型则都定义在MetricEvent枚举中。

执行addPass方法对应LongAdder数组索引下表为0的值递增

下面再来看下data.currentWindow()的内部逻辑

OccupiableBucketLeapArray

OccupiableBucketLeapArray继承了抽象类LeapArray,核心逻辑也是在LeapArray


/** * 时间窗口大小 单位ms  */ protected int windowLengthInMs; /** * 切分的窗口数  */ protected int sampleCount; /**  * 统计的时间间隔 intervalInMs = windowLengthInMs * sampleCount  */  protected int intervalInMs; /**  * 窗口数组 数组大小 = sampleCount  */ protected final AtomicReferenceArray<WindowWrap<T>> array; /**  * update lock 更新窗口时需要上锁  */ private final ReentrantLock updateLock = new ReentrantLock(); /**  * @param sampleCount 需要划分的窗口数  * @param intervalInMs 间隔的统计时间  */ public LeapArray(int sampleCount, int intervalInMs) { this.windowLengthInMs = intervalInMs / sampleCount; this.intervalInMs = intervalInMs; this.sampleCount = sampleCount; this.array = new AtomicReferenceArray<>(sampleCount); } /** * 获取当前窗口  */ public WindowWrap<T> currentWindow() { return currentWindow(TimeUtil.currentTimeMillis()); }

以上需要着重理解的是几个参数的含义:

  1. sampleCount 定义的窗口的数

  2. intervalInMs 统计的时间间隔

  3. windowLengthInMs 每个窗口的时间大小 = intervalInMs / sampleCount

sampleCount 比较好理解,就是需要定义几个窗口(默认秒级统计维度的话是两个窗口),intervalInMs 指的就是我们需要统计的时间间隔,例如我们统计QPS的话那就是1000ms,windowLengthInMs 指的每个窗口的大小,是由intervalInMs除以sampleCount得来

类似下图

理解了上诉几个参数的含义后,我们直接进入到LeapArraycurrentWindow(long time)方法中去看看具体的实现


public WindowWrap<T> currentWindow(long timeMillis) { if (timeMillis < 0) { return null; } // 根据当前时间戳计算当前所属的窗口数组索引下标 int idx = calculateTimeIdx(timeMillis); // 计算当前窗口的开始时间戳 long windowStart = calculateWindowStart(timeMillis); /* * 从窗口数组中获取当前窗口项,分为三种情况 * * (1) 当前窗口为空还未创建,则初始化一个 * (2) 当前窗口的开始时间和上面计算出的窗口开始时间一致,表明当前窗口还未过期,直接返回当前窗口 * (3) 当前窗口的开始时间 小于 上面计算出的窗口开始时间,表明当前窗口已过期,需要替换当前窗口 */ while (true) { WindowWrap<T> old = array.get(idx); if (old == null) { /* * 第一种情况,新建一个窗口项 */ WindowWrap<T> window = new WindowWrap<T>(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()) { /* * 第二种情况 直接返回 */ 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); } 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()) { // 第四种情况,讲道理不会走到这里 return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis)); } } } /** * 根据当前时间戳计算当前所属的窗口数组索引下标  */ private int calculateTimeIdx(/*@Valid*/ long timeMillis) { long timeId = timeMillis / windowLengthInMs; return (int)(timeId % array.length()); } /** * 计算当前窗口的开始时间戳  */ protected long calculateWindowStart(/*@Valid*/ long timeMillis) { return timeMillis - timeMillis % windowLengthInMs; }

上面的方法就是整个滑动窗口逻辑的核心代码,注释其实也写的比较清晰了,简单概括下可以分为以下几步:

  1. 根据当前时间戳 和 窗口数组大小 获取到当前的窗口数组索引下标idx,如果窗口数是2,那其实idx只有两种值(0 或 1)

  2. 根据当前时间戳(windowStart) 计算得到当前窗口的开始时间戳值。通过calculateWindowStart计算来得到,这个方法还蛮有意思的,通过当前时间戳和窗口时间大小取余来得到 与当前窗口开始时间的 偏移量。比我用定时任务实现高级多了 ...

    Sentinel源码解析三(滑动窗口流量统计)的更多相关文章

    1. Sentinel源码解析四(流控策略和流控效果)

      引言 在分析Sentinel的上一篇文章中,我们知道了它是基于滑动窗口做的流量统计,那么在当我们能够根据流量统计算法拿到流量的实时数据后,下一步要做的事情自然就是基于这些数据做流控.在介绍Sentin ...

    2. Sentinel源码解析二(Slot总览)

      写在前面 本文继续来分析Sentinel的源码,上篇文章对Sentinel的调用过程做了深入分析,主要涉及到了两个概念:插槽链和Node节点.那么接下来我们就根据插槽链的调用关系来依次分析每个插槽(s ...

    3. Sentinel源码解析一(流程总览)

      引言 Sentinel作为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性.相比于Hystrix,Sentinel的设计更加简 ...

    4. Celery 源码解析三: Task 对象的实现

      Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个:第二个位于 celery/task/base.py 中,这是第二个.他们之间是有关系的, ...

    5. Mybatis源码解析(三) —— Mapper代理类的生成

      Mybatis源码解析(三) -- Mapper代理类的生成   在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...

    6. ReactiveCocoa源码解析(三) Signal代码的基本实现

      上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

    7. ReactiveSwift源码解析(三) Signal代码的基本实现

      上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...

    8. React的React.createRef()/forwardRef()源码解析(三)

      1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...

    9. 透过 NestedScrollView 源码解析嵌套滑动原理

      NestedScrollView 是用于替代 ScrollView 来解决嵌套滑动过程中的滑动事件的冲突.作为开发者,你会发现很多地方会用到嵌套滑动的逻辑,比如下拉刷新页面,京东或者淘宝的各种商品页面 ...

    随机推荐

    1. GitHub 热点速览 Vol.17:在?各家视频会员要不要?

      作者:HelloGitHub-小鱼干 摘要:经济实用,用作上周的 GitHub 热点的横批再合适不过.先不说 GitHub Trending 上不止一个的会员共享项目,免你找好友刷脸要会员,这项目实在 ...

    2. 在Windows中使用VirtualBox安装Ubuntu

      VeitualBox官网下载:https://www.virtualbox.org/wiki/Downloads 安装教程:http://dblab.xmu.edu.cn/blog/337-2/ 安装 ...

    3. input type file onchange上传文件的过程中,同一个文件二次上传无效的问题。

      不要采用删除当前input[type=file]这个节点,然后再重新创建dom这种方案,这样是不合理的.解释如下:input[type=file]使用的是onchange去做,onchange监听的为 ...

    4. docker commit理解构建镜像(7)

      镜像是多层存储,每一层是在前一层的基础上进行的修改: 而容器同样也是多层存储是在以镜像为基础层,在基础层上加一层作为容器运行时的存储层. 当我们使用Docker Hub的镜像无法满足我们的需求时,我们 ...

    5. Linux C语言 检测文件是否存在

      头文件 unistd.h ) { // file exists } else { // file doesn't exist } You can also use R_OK, W_OK, and X_ ...

    6. linux 二级目录结构

      Linux系统里面目录的顶点都是根 /etc /etc/passwd : Linux用户登陆的文件 /etc/group : 存放Linux用户组的文件 /etc/shadow :存放用户密码的文件 ...

    7. Ngxin 开启CDN 日志获取不了真实IP的解决办法。

      nginx配置里面在http{ 后加入如下两行代码即可: set_real_ip_from 0.0.0.0/0;real_ip_header X-Forwarded-For; 重启nginx生效. 注 ...

    8. POJ - 2387 Til the Cows Come Home (最短路入门)

      Bessie is out in the field and wants to get back to the barn to get as much sleep as possible before ...

    9. POJ2421 Constructing Roads 最小生成树

      修路 时限: 2000MS   内存限制: 65536K 提交总数: 31810   接受: 14215 描述 有N个村庄,编号从1到N,您应该修建一些道路,使每两个村庄可以相互连接.我们说两个村庄A ...

    10. python——random.sample()的用法

      写脚本过程中用到了需要随机一段字符串的操作,查了一下资料,对于random.sample的用法,多用于截取列表的指定长度的随机数,但是不会改变列表本身的排序: list = [0,1,2,3,4] r ...