系列文章目录
【spring系列】Alibaba Sentinel(1)快速启动
【spring系列】Alibaba Sentinel(2)流控详解
【spring系列】Alibaba Sentinel(3)降级详解
前言
之前简单的使用了下sentinel,通过sentinel实现了简单的流控降级的示例,sentinel真正的奥妙缺在这个滑动窗口,通过滑动窗口来记录每个时间段是否可以通过限制。
提示:以下是本篇文章正文内容,下面案例可供参考
一、滑动窗口实现原理
1. 实现思路
Sentinel 底层采用高性能的滑动窗口数据结构 LeapArray 来统计实时的秒级指标数据,可以很好地支撑写多于读的高并发场景。
2.源码解析
LeapArray表示整个滑动窗口,其中有几个很重要的属性
代码如下(示例):
//单口滑动时间窗口的大小,单位是毫秒
//最小的单位,如上所图,这个值应该是200
protected int windowLengthInMs;
//滑动时间窗口个数 5
protected int sampleCount;
//统计时间区间 1秒
protected int intervalInMs;
// 保存统计数据的地方,数组中5 个窗口
protected final AtomicReferenceArray<WindowWrap<T>> array;
LeapArray中规定了窗口的基本属性,WindowWrap中保存了窗口的信息
/**
* 以毫秒为单位的时间窗口长度
*/
private final long windowLengthInMs;
/**
* 窗口的开始时间,例如左边窗口的值为200
*/
private long windowStart;
/**
* 数据统计,比如1秒中通过了几条请求,拦截了几条请求等等。
*/
private T value;
LeapArray通过传入当前时间获取当前时间窗口,calculateTimeIdx获取窗口index;calculateWindowStart获取当前窗口的开始时间。
public WindowWrap<T> currentWindow(long timeMillis) {
//当前时间戳,肯定不能小于0拉
if (timeMillis < 0) {
return null;
}
//获取当前的时间窗口index
int idx = calculateTimeIdx(timeMillis);
// Calculate current bucket start time.
//获取当前时间窗口的开始时间
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) {
WindowWrap<T> old = array.get(idx);
//如果当前窗口不存在,一般刚刚开始时候为null
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.
*/
// 设置时间窗口的属性,窗口长度,窗口开始时间等等
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
//通过CAS进行原子更新
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()) {
//在同一个时间窗口,比如800为开始时间,第二个请求进来的时候,时间为888,
/*
* B0 B1 B2 B3 B4
* ||_______|_______|_______|_______|_______||___
* 200 400 600 800 1000 1200 timestamp
* ^
* time=888
*/
return old;
} else if (windowStart > old.windowStart()) {
// 不在同一个时间窗口的话,旧的窗口就没有作用拉,old窗口说明是旧的信息,则需要进行重置
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()) {
// 一般这种情况不会存在,比如第一次设置的时间为12点整,第二次获取的时间为11点59分
//理论上是不存在这种情况的
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}
calculateTimeIdx
//获取当前时间的时间窗口的index
private int calculateTimeIdx(/*@Valid*/ long timeMillis) {
// * B0 B1 B2 NULL B4
// * ||_______|_______|_______|_______|_______||___
// * 200 400 600 800 1000 1200 timestamp
// | window为1秒 |
// * ^
// * time=888
// 比如每个节点的间隔是200 ,当前时间为888,要获取当前时间在哪个节点中
long timeId = timeMillis / windowLengthInMs;
// Calculate current index so we can map the timestamp to the leap array.
// 返回时间窗口的下标。如果有2个窗口,通过求余,来判断当前节点在哪个窗口中
return (int)(timeId % array.length());
}
MetricBucket 为LeapArray的泛型类 ,意思就是指标桶,,可以认为一个 MetricBucket 对象可以存储一个抽样时间段内所有的指标,例如一个抽象时间段中通过数量、阻塞数量、异常数量、成功数量、响应时间。通过LongAdder
进行计数。一般多线程计数都使用CAS进行增加,比如AtomicInteger
,LongAdder
是Atomic的一种优化。