Sentinel笔记-滑动窗口
服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,至少确保服务不会奔溃。
常见的服务降级实现方式有:
开关降级、
人工或者设置定时开关,接口直接返回默认值,适用于促销活动等可以明确预估到并发会突增的场景。
限流降级、
假设服务 A 需要依赖服务 B 完成客户端的一次请求,服务 B 处理最大并发请求就是限流,超过最大QPS,B就直接拒绝以保护自己。
降级方式包括:快速失败,warm up,排队等待
熔断降级。
假设服务 A 需要依赖服务 B 完成客户端的一次请求,在服务 B“不行”的时候不再去请求服务 B,这就是熔断,并且B恢复后A需要感知得到,所以熔断需要一个时间周期。
常见熔断策略:慢调用比例,异常比例,异常数量
Sentinel与 Hystrix对比
参照: https://mp.weixin.qq.com/s/12mjY9KawMoyc_DjC883uQ
Sentinel 是基于滑动窗口实现的实时指标数据统计
AtomicReferenceArray
提供了可以原子读取和写入的底层引用数组的操作,并且还包含高级原子操作。如:getAndSet,compareAndSet等。
滑动窗口具体实现类结构图
Bucket 记录一段时间内的各项指标数据用的是一个 LongAdder 数组,LongAdder 保证了数据修改的原子性,并且性能比 AtomicInteger 表现更好。数组的每个元素分别记录一个时间窗口内的请求总数、异常数、总耗时。
Sentinel 用枚举类型 MetricEvent 的 ordinal 属性作为下标,ordinal 的值从 0 开始,按枚举元素的顺序递增,正好可以用作数组的下标。
public enum MetricEvent {
PASS,
BLOCK,
EXCEPTION,
SUCCESS,
RT,
OCCUPIED_PASS
}
public class MetricBucket {
private final LongAdder[] counters;
//直接使用MetricEvent作为index操作数组
public MetricBucket add(MetricEvent event, long n) {
counters[event.ordinal()].add(n);
return this;
}
public long pass() {
return get(MetricEvent.PASS);
}
}
滑动窗口:
如果我们希望能够知道某个接口的每秒处理成功请求数(成功 QPS)、每秒处理失败请求数(失败 QPS),以及处理每个成功请求的平均耗时(avg RT),我们只需要控制 Bucket 统计一秒钟的指标数据即可。
Bucket不可能无限大,当我们只需要保留一分钟的数据时,Bucket 数组的大小就可以设置为 60,我们希望这个数组可以循环使用,并且永远只保存最近 1 分钟的数据,这样不仅可以避免频繁的创建 Bucket,也减少内存资源的占用。但是Bucket不可能无限大,所以使用了
因为使用循环数组存储数据,涉及到以下问题,定位和判断过期
定位:
private int calculateTimeIdx( long timeMillis) {
//当前时间戳一共经历了多少bucket,当前时间戳/bucket时长
long timeId = timeMillis / windowLengthInMs;
//计算当前时间戳落在数组具体位置
return (int) (timeId % array.length());
}
/**
* 计算当前时间对应的窗口的开始时间,当前时间戳减去(之前完整的bucket数量)
* 获取bucket开始时间戳, 去掉毫秒部分
*/
protected long calculateWindowStart(long timeMillis) {
/**
* 假设窗口大小为1000毫秒,即数组每个元素存储1秒钟的统计数据
* timeMillis % windowLengthInMs 就是取得毫秒部分
* timeMillis - 毫秒数 = 秒部分
* 这就得到每秒的开始时间戳
*/
return timeMillis - timeMillis % windowLengthInMs;
}
因为 Bucket 自身并不保存时间窗口信息,所以 Sentinel 给 Bucket 加了一个包装类 WindowWrap,用于记录 Bucket 的时间窗口信息,WindowWrap 源码如下。
只要知道时间窗口的开始时间和窗口时间大小,只需要给定一个时间戳,就能知道该时间戳是否在 Bucket 的窗口时间内,见方法 isTimeInWindow
public class WindowWrap<T> {
/** * 窗口时间长度(毫秒) */
private final long windowLengthInMs;
/** * 开始时间戳(毫秒) */
private long windowStart;
/** * 统计数据,实际上是类 MetricBucket */
private T value;
/** 检查给定的时间戳是否在当前 bucket 中。 */
public boolean isTimeInWindow(long timeMillis) {
return windowStart <= timeMillis && timeMillis < windowStart + windowLengthInMs;
}
}
通过时间戳定位 Bucket 当接收到一个请求时,可根据接收到请求的时间戳计算出一个数组索引,从滑动窗口(WindowWrap 数组)中获取一个 WindowWrap,从而获取 WindowWrap 包装的 Bucket,调用 Bucket 的 add 方法记录相应的事件。 核心方法如下:
/**
* 根据时间戳获取 bucket
*
* @param timeMillis 时间戳(毫秒)
* @return 如果时间有效,则在提供的时间戳处显示当前存储桶项;如果时间无效,则为空
*/
public WindowWrap<T> currentWindow(long timeMillis) {
if (timeMillis < 0) {
return null;
}
// 获取时间戳映射到的数组索引
int idx = calculateTimeIdx(timeMillis);
// 计算 bucket 时间窗口的开始时间
long windowStart = calculateWindowStart(timeMillis);
// 从数组中获取 bucket
while (true) {
WindowWrap<T> old = array.get(idx);
// 一般是项目启动时,时间未到达一个周期,数组还没有存储满,没有到复用阶段,所以数组元素可能为空
if (old == null) {
// 创建新的 bucket,并创建一个 bucket 包装器
WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
// cas 写入,确保线程安全,期望数组下标的元素是空的,否则就不写入,而是复用
if (array.compareAndSet(idx, null, window)) {
return window;
} else {
Thread.yield();
}
}
// 如果 WindowWrap 的 windowStart 正好是当前时间戳计算出的时间窗口的开始时间,则就是我们想要的 bucket
else if (windowStart == old.windowStart()) {
return old;
}
// 复用旧的 bucket
else if (windowStart > old.windowStart()) {
if (updateLock.tryLock()) {
try {
// 重置 bucket,并指定 bucket 的新时间窗口的开始时间
return resetWindowTo(old, windowStart);
} finally {
updateLock.unlock();
}
} else {
Thread.yield();
}
}
// 计算出来的当前 bucket 时间窗口的开始时间比数组当前存储的 bucket 的时间窗口开始时间还小,
// 直接返回一个空的 bucket 就行
else if (windowStart < old.windowStart()) {
return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
}
}
}