前言

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. Android 开发技术周报 Issue#277

      新闻 Android 11界面再调整:加入快速截屏.多任务向国产ROM看齐 最新版Android 11推送 谷歌Pixel 5被曝光:支持反向充电 4月Android系统版本分布:8.0 Oreo最主

    2. 详解 Discuz 的 PHP经典加密解密函数 authcode

      函数注释: // $string: 明文 或 密文 // $operation:DECODE表示解密,其它表示加密 // $key: 密匙 // $expiry:密文有效期 function auth ...

    3. DZ的CURD

      Discuz二次开发-MySQL插入数据(insert) DB::insert($tableName,$data,$flag); $tableName:表名 $data:插入数据,以字段为键值的关联数 ...

    4. 2019-2020-1 20199329《Linux内核原理与分析》第一周作业

      Linux学习随笔 Linux 是一个操作系统,我们的 Linux 主要是系统调用和内核那两层. UNIX前身是Multics,但 UNIX 的商业版本非常昂贵,于是Linus Torvalds(Li ...

    5. Spring5参考指南:Bean的创建

      文章目录 Spring容器中的Bean Bean的命名 Bean的实例化 Spring容器中的Bean Bean在Spring中就是一个业务组件,我们通过创建各种Bean来完成最终的业务逻辑功能. 在 ...

    6. Scala教程之:静态类型

      文章目录 泛类型 型变 协变 逆变 不变 类型上界 类型下界 内部类 抽象类型 复合类型 自类型 隐式参数 隐式转换 多态方法 类型推断 Scala是静态类型的,它拥有一个强大的类型系统,静态地强制以 ...

    7. c语言解一元二次方程

      C语言解一元二次方程,输入系数a,b,c; #include <stdio.h> #include <math.h> int main(int argc, char *argv ...

    8. javascript SDK开发之webpack打包支持对象展开运算符...

      场景:使用了对象的展开运算符,webpack打包报错,错误如下:Parsing error: Unexpected token - 1.安装依赖 npm install babel-preset-st ...

    9. 从实践出发:微服务布道师告诉你Spring Cloud与Boot他如何选择

      背景 随着公司业务量的飞速发展,平台面临的挑战已经远远大于业务,需求量不断增加,技术人员数量增加,面临的复杂度也大大增加.在这个背景下,平台的技术架构也完成了从传统的单体应用到微服务化的演进. 系统架 ...

    10. Python之路【第二十八篇】:生成器与迭代器

      #!/usr/bin/env python # -*- coding:utf-8 -*- #只要函数的代码里面出现了yield关键字,这个函数就不再是一个普通的函数了,叫做生成器函数 #执行生成器函数 ...