Sentinel源码解析三(滑动窗口流量统计)
前言
Sentinel
的核心功能之一是流量统计,例如我们常用的指标QPS,当前线程数等。上一篇文章中我们已经大致提到了提供数据统计功能的Slot(StatisticSlot)
,StatisticSlot
在Sentinel
的整个体系中扮演了一个非常重要的角色,后续的一系列操作(限流,熔断)等都依赖于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
主要统计了两种类型的数据
线程数
请求数(QPS)
对于线程数的统计比较简单,通过内部维护一个LongAdder
来进行当前线程数的统计,每进入一个请求加1,每释放一个请求减1,从而得到当前的线程数
对于请求数QPS的统计则相对比较复杂,其中有用到滑动窗口原理(也是本文的重点),下面根据源码来深入的分析
DefaultNode和StatisticNode
public void addPassRequest(int count) {
// 调用父类(StatisticNode)来进行统计
super.addPassRequest(count);
// 根据clusterNode 汇总统计(背后也是调用父类StatisticNode)
this.clusterNode.addPassRequest(count);
}
最终都是调用了父类StatisticNode
的addPassRequest
方法
/**
* 按秒统计,分成两个窗口,每个窗口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);
}
代码比较简单,可以知道内部是调用了ArrayMetric
的addPass
方法来统计的,并且统计了两种不同时间维度的数据(秒级和分钟级)
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
是一个抽象类,OccupiableBucketLeapArray
和BucketLeapArray
都是其具体的实现类
OccupiableBucketLeapArray
在1.5版本之后才被引入,主要是为了解决一些高优先级的请求在限流触发的时候也能通过(通过占用未来时间窗口的名额来实现) 也是默认使用的LeapArray实现类
而统计的逻辑也比较清楚,分成了两步:
定位到当前窗口
获取到当前窗口
WindowWrap
的MetricBucket
并执行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());
}
以上需要着重理解的是几个参数的含义:
sampleCount 定义的窗口的数
intervalInMs 统计的时间间隔
windowLengthInMs 每个窗口的时间大小 = intervalInMs / sampleCount
sampleCount
比较好理解,就是需要定义几个窗口(默认秒级统计维度的话是两个窗口),intervalInMs
指的就是我们需要统计的时间间隔,例如我们统计QPS的话那就是1000ms,windowLengthInMs
指的每个窗口的大小,是由intervalInMs
除以sampleCount
得来
类似下图
理解了上诉几个参数的含义后,我们直接进入到LeapArray
的currentWindow(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;
}
上面的方法就是整个滑动窗口逻辑的核心代码,注释其实也写的比较清晰了,简单概括下可以分为以下几步:
根据当前时间戳 和 窗口数组大小 获取到当前的窗口数组索引下标
idx
,如果窗口数是2,那其实idx
只有两种值(0 或 1)根据当前时间戳(
windowStart
) 计算得到当前窗口的开始时间戳值。通过calculateWindowStart
计算来得到,这个方法还蛮有意思的,通过当前时间戳和窗口时间大小取余来得到 与当前窗口开始时间的 偏移量。比我用定时任务实现高级多了 ...Sentinel源码解析三(滑动窗口流量统计)的更多相关文章
- Sentinel源码解析四(流控策略和流控效果)
引言 在分析Sentinel的上一篇文章中,我们知道了它是基于滑动窗口做的流量统计,那么在当我们能够根据流量统计算法拿到流量的实时数据后,下一步要做的事情自然就是基于这些数据做流控.在介绍Sentin ...
- Sentinel源码解析二(Slot总览)
写在前面 本文继续来分析Sentinel的源码,上篇文章对Sentinel的调用过程做了深入分析,主要涉及到了两个概念:插槽链和Node节点.那么接下来我们就根据插槽链的调用关系来依次分析每个插槽(s ...
- Sentinel源码解析一(流程总览)
引言 Sentinel作为ali开源的一款轻量级流控框架,主要以流量为切入点,从流量控制.熔断降级.系统负载保护等多个维度来帮助用户保护服务的稳定性.相比于Hystrix,Sentinel的设计更加简 ...
- Celery 源码解析三: Task 对象的实现
Task 的实现在 Celery 中你会发现有两处,一处位于 celery/app/task.py,这是第一个:第二个位于 celery/task/base.py 中,这是第二个.他们之间是有关系的, ...
- Mybatis源码解析(三) —— Mapper代理类的生成
Mybatis源码解析(三) -- Mapper代理类的生成 在本系列第一篇文章已经讲述过在Mybatis-Spring项目中,是通过 MapperFactoryBean 的 getObject( ...
- ReactiveCocoa源码解析(三) Signal代码的基本实现
上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...
- ReactiveSwift源码解析(三) Signal代码的基本实现
上篇博客我们详细的聊了ReactiveSwift源码中的Bag容器,详情请参见<ReactiveSwift源码解析之Bag容器>.本篇博客我们就来聊一下信号量,也就是Signal的的几种状 ...
- React的React.createRef()/forwardRef()源码解析(三)
1.refs三种使用用法 1.字符串 1.1 dom节点上使用 获取真实的dom节点 //使用步骤: 1. <input ref="stringRef" /> 2. t ...
- 透过 NestedScrollView 源码解析嵌套滑动原理
NestedScrollView 是用于替代 ScrollView 来解决嵌套滑动过程中的滑动事件的冲突.作为开发者,你会发现很多地方会用到嵌套滑动的逻辑,比如下拉刷新页面,京东或者淘宝的各种商品页面 ...
随机推荐
- GitHub 热点速览 Vol.17:在?各家视频会员要不要?
作者:HelloGitHub-小鱼干 摘要:经济实用,用作上周的 GitHub 热点的横批再合适不过.先不说 GitHub Trending 上不止一个的会员共享项目,免你找好友刷脸要会员,这项目实在 ...
- 在Windows中使用VirtualBox安装Ubuntu
VeitualBox官网下载:https://www.virtualbox.org/wiki/Downloads 安装教程:http://dblab.xmu.edu.cn/blog/337-2/ 安装 ...
- input type file onchange上传文件的过程中,同一个文件二次上传无效的问题。
不要采用删除当前input[type=file]这个节点,然后再重新创建dom这种方案,这样是不合理的.解释如下:input[type=file]使用的是onchange去做,onchange监听的为 ...
- docker commit理解构建镜像(7)
镜像是多层存储,每一层是在前一层的基础上进行的修改: 而容器同样也是多层存储是在以镜像为基础层,在基础层上加一层作为容器运行时的存储层. 当我们使用Docker Hub的镜像无法满足我们的需求时,我们 ...
- Linux C语言 检测文件是否存在
头文件 unistd.h ) { // file exists } else { // file doesn't exist } You can also use R_OK, W_OK, and X_ ...
- linux 二级目录结构
Linux系统里面目录的顶点都是根 /etc /etc/passwd : Linux用户登陆的文件 /etc/group : 存放Linux用户组的文件 /etc/shadow :存放用户密码的文件 ...
- Ngxin 开启CDN 日志获取不了真实IP的解决办法。
nginx配置里面在http{ 后加入如下两行代码即可: set_real_ip_from 0.0.0.0/0;real_ip_header X-Forwarded-For; 重启nginx生效. 注 ...
- 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 ...
- POJ2421 Constructing Roads 最小生成树
修路 时限: 2000MS 内存限制: 65536K 提交总数: 31810 接受: 14215 描述 有N个村庄,编号从1到N,您应该修建一些道路,使每两个村庄可以相互连接.我们说两个村庄A ...
- python——random.sample()的用法
写脚本过程中用到了需要随机一段字符串的操作,查了一下资料,对于random.sample的用法,多用于截取列表的指定长度的随机数,但是不会改变列表本身的排序: list = [0,1,2,3,4] r ...
- Sentinel源码解析四(流控策略和流控效果)